
External sharing is a compliance nightmare. Your 365 tenant has dozens of SharePoint sites, each with document libraries, and files are shared via links or direct invitations. The SharePoint admin center shows you a high-level sharing policy per site, but it won’t tell you which specific files are shared externally or with whom.
I wrote a script that walks every site, every drive, and every file in your tenant using the Microsoft Graph API, then exports a CSV of every externally shared file with who it’s shared with.
Why the Graph API?
The older PnP PowerShell modules can tell you about site-level sharing settings, but file-level permission details require Graph. The Graph endpoints for drive items and permissions give you the actual invitation emails, link scope, and grantee identities.
Prerequisites
You’ll need the Microsoft Graph PowerShell SDK and an account with at least Sites.Read.All and Directories.Read.All scope:
Install-Module Microsoft.Graph
Connect-MgGraph -Scopes "Sites.Read.All", "Directory.Read.All"
For read-only auditing, these scopes are all you need (no global admin required).
Step 1: Enumerate All Sites
$sites = Get-MgSite -All
This returns every SharePoint site and OneDrive for Business drive in the tenant. The -All flag handles pagination automatically.
Step 2: Get Drives Per Site
Each site can have multiple document libraries (drives). You need to check them all:
foreach ($site in $sites) {
$drives = Get-MgSiteDrive -SiteId $site.Id
foreach ($drive in $drives) {
# ... check each drive
}
}
Step 3: Find Shared Items
This is the key filter — only items that have been shared show up:
$items = Get-MgDriveItem -DriveId $drive.Id -Filter "Shared ne null" -PageSize 5000
The -Filter "Shared ne null" clause is server-side and dramatically reduces the result set. Without it, you’d be pulling every file in every library just to check a flag. The -PageSize 5000 maximizes the batch size to reduce API round trips.
Step 4: Inspect Permissions
For each shared item, the script pulls the detailed permissions and checks two conditions:
- Direct invitations — someone outside the organization was explicitly invited (
$permission.Invitation.Email) - External links — a sharing link was created with a scope broader than
"organization"($permission.Link.Scope)
$permissions = Get-MgDriveItemPermission -DriveId $drive.Id -DriveItemId $item.Id
foreach ($permission in $permissions) {
if ($null -ne $permission.Invitation.Email -or
($null -ne $permission.Link.Scope -and "organization" -ne $permission.Link.Scope)) {
# This file is shared externally
}
}
Step 5: Resolve Grantees
Graph returns grantee information in one of two property paths depending on how the file was shared. The script checks both:
if ($null -ne $permission.GrantedToIdentities) {
$SharedWithUser = $permission.GrantedToIdentities.User.DisplayName -join ", "
} elseif ($null -ne $permission.GrantedTo) {
$SharedWithUser = $permission.GrantedTo.User.DisplayName -join ", "
}
This captures users, applications, and devices that have been granted access.
Step 6: Build the Report
Each externally shared file becomes a structured object with the site name, drive name, file name, share scope, share type, web URL, and grantee details:
$SharedFile = [pscustomobject]@{
SiteName = $site.DisplayName
DriveName = $drive.Name
FileName = $item.Name
FileId = $item.Id
DriveId = $drive.Id
ShareScope = $permission.Link.Scope
ShareType = $permission.Link.Type
WebUrl = $permission.Link.WebUrl
SharedWithUser = $SharedWithUser
SharedWithApp = $SharedWithApp
SharedWithDevice = $SharedWithDevice
InvitationEmail = $permission.Invitation.Email
}
At the end, everything exports to CSV:
$externallySharedFiles | Export-Csv -Path "ExternallySharedFiles.csv" -NoTypeInformation
The Full Script
# Get SharePoint or OneDrive sites (adjust for specific sites or drives if needed)
$sites = Get-MgSite -All
# Initialize an array to store results
$externallySharedFiles = @()
# Loop through each site and its drives (OneDrive and SharePoint sites)
foreach ($site in $sites) {
Write-Host "Checking site: $($site.DisplayName)"
# Get all drives (document libraries) within the site
$drives = Get-MgSiteDrive -SiteId $site.Id
foreach ($drive in $drives) {
# Get all items (files and folders) from the drive
$items = Get-MgDriveItem -DriveId $drive.Id -Filter "Shared ne null" -PageSize 5000
Write-Host "Checking drive: $($drive.Name) - Count: $($items.Count)"
foreach ($item in $items) {
# Check if the item has any sharing permissions or links
$permissions = Get-MgDriveItemPermission -DriveId $drive.Id -DriveItemId $item.Id
foreach ($permission in $permissions) {
if ($null -ne $permission.Invitation.Email -or
($null -ne $permission.Link.Scope -and "organization" -ne $permission.Link.Scope)) {
Write-Host "Externally shared files found $($permission.Invitation.Email)"
if ($null -ne $permission.GrantedToIdentities) {
$SharedWithUser = $permission.GrantedToIdentities.User.DisplayName -join ", "
$SharedWithApp = $permission.GrantedToIdentities.Application.DisplayName -join ", "
$SharedWithDevice = $permission.GrantedToIdentities.Device.DisplayName -join ", "
} elseif ($null -ne $permission.GrantedTo) {
$SharedWithUser = $permission.GrantedTo.User.DisplayName -join ", "
$SharedWithApp = $permission.GrantedTo.Application.DisplayName -join ", "
$SharedWithDevice = $permission.GrantedTo.Device.DisplayName -join ", "
}
$SharedFile = [pscustomobject]@{
SiteName = $site.DisplayName
DriveName = $drive.Name
FileName = $item.Name
FileId = $item.Id
DriveId = $drive.Id
ShareScope = $permission.Link.Scope
ShareType = $permission.Link.Type
WebUrl = $permission.Link.WebUrl
SharedWithUser = $SharedWithUser
SharedWithApp = $SharedWithApp
SharedWithDevice = $SharedWithDevice
InvitationEmail = $permission.Invitation.Email
}
$SharedFile
# The file has been shared externally
$externallySharedFiles += $SharedFile
}
}
}
}
}
# Output the externally shared files
if ($externallySharedFiles.Count -gt 0) {
Write-Host "Externally shared files found:"
$externallySharedFiles | Format-Table -AutoSize
} else {
Write-Host "No externally shared files found."
}
# Optionally, export results to CSV
$externallySharedFiles | Export-Csv -Path "ExternallySharedFiles.csv" -NoTypeInformation
I’ve also uploaded a copy on GitHub as a gist here.
Performance Considerations
This script is thorough, not fast. For a large tenant with hundreds of sites and millions of files, it will take time. Here are some ways to make it practical:
- Scope to specific sites — replace
Get-MgSite -AllwithGet-MgSite -Search "Site Name"to target a subset - Add rate limiting — Graph has per-user and per-tenant throttling. If you hit 429 errors, add
Start-Sleep -Seconds 2between the inner loop iterations - Run it incrementally — add a
-ModifiedSincefilter to only check files changed in the last 30 days - Use app authentication — user-scoped Graph calls are more aggressively throttled than app-registered calls
When to Run This
- Before changing your tenant’s external sharing policy (to know what will break)
- As part of a quarterly compliance review
- After a security incident to find data exfiltration paths
- During onboarding to understand a new client’s exposure





