Entra / Microsoft 365 ยท SharePoint & OneDrive
Report sharing OneDrive files
Report permissions for shared files found in OneDrive for Business accounts.
Connect & set up
Run these once per session. All scopes are read-only unless the script makes changes.
Connect-MgGraph -AppId $AppId -TenantId $TenantId -CertificateThumbprint $Thumbprint -NoWelcome
Run it
The main script. Copy it, or download the .ps1 and run it from your console.
param([string] $TenantId = "",[string] $AppId = "")function Get-DriveItems {[CmdletBinding()]param ([Parameter()]$Drive,[Parameter()]$FolderId)# Get data for a folder and its children[array]$Data = Get-MgDriveItemChild -DriveId $Drive -DriveItemId $FolderId -All# Split the data into files and folders[array]$Folders = $Data | Where-Object {$_.folder.childcount -gt 0} | Sort-Object Name$Global:TotalFolders = $TotalFolders + $Folders.Count[array]$Files = $Data | Where-Object {$null -ne $_.file.mimetype}$TotalFiles = $TotalFiles + $Files.Count[array]$SharedItems = $Files | Where-Object {$null -ne $_.Shared.Scope}If ($null -eq $Folder.Name) {$FolderName = "the OneDrive account root"} Else {$FolderName = ("folder: {0}" -f $Folder.Name)}# Process the filesForEach ($File in $SharedItems) {If ($File.LastModifiedDateTime) {$LastModifiedDateTime = Get-Date $File.LastModifiedDateTime -format 'dd-MMM-yyyy HH:mm'} Else {$LastModifiedDateTime = $null}If ($File.CreatedDateTime) {$FileCreatedDateTime = Get-Date $File.CreatedDateTime -format 'dd-MMM-yyyy HH:mm'}[array]$Permissions = Get-MgDriveItemPermission -DriveId $Drive -DriveItemId $File.Id `-Property Roles, GrantedTo, HasPassword, ExpirationDateTime, Invitation, InheritedFrom, Id, Link, GrantedToV2 | Sort-Object Roles[array]$GrantedPermissions = $Permissions | Where-Object {$_.Roles[0] -ne 'owner'}If ($GrantedPermissions) {ForEach ($Permission in $GrantedPermissions) {$Role = ($Permission.Roles -join ",").trim()Switch ($Role) {"read" {$Scope = "View only"}"write" {$Scope = "Edit"}default {$Scope = "View only"}}$GrantedTo = $null; $GrantedToId = $null# Use GrantToV2 if it's availableIf ($Permission.GrantedToV2.User.DisplayName) {$GrantedTo = $Permission.GrantedToV2.User.DisplayName$GrantedToId = $Permission.Grantedtov2.SiteUser.LoginName.Split("|")[2]} Else {$GrantedTo = $Permission.GrantedTo.User.DisplayName$GrantedToId = $Permission.GrantedTo.User.Id}# Handle the different types of sharing link$LinkExpired = $nullIf ($Permission.link.scope) {If ($Permission.ExpirationDateTime) {If ($Permission.ExpirationDateTime -lt [datetime]::Now) {$ExpirationDate = ("Link expired on: {0}" -f $Permission.ExpirationDateTime)$LinkExpired = $true} Else {$ExpirationDate = ("Link expires on: {0}" -f $Permission.ExpirationDateTime)$LinkExpired = $false}} Else {$ExpirationDate = $null}$GrantedToId = "N/A"Switch ($Permission.link.scope) {"anonymous" {$GrantedTo = "Anyone link"$GrantedToId = "Anyone with the link"}"organization" {$GrantedTo = "Organization link"$GrantedToId = "Anyone in the tenant"}"existingAccess" {$GrantedTo = "Existing access link"}"users" {$GrantedTo = "Specific set of users"$GrantedToId = "Users with the link"}}}# Resolve values returned for accounts that no longer exist and guest accountsIf (Test-Numeric ($GrantedToId)) {$GrantedToId = "User account removed from tenant"} ElseIf ($GrantedToId -like "*#EXT#*") {$GrantedToId = Get-MgUser -UserId $GrantedToId | Select-Object -ExpandProperty Mail} ElseIf ([guid]::TryParse($GrantedToId, $([ref][guid]::Empty))) {# The identifier is a GUID rather than a UPN, so the sharing is with a group. Check the hash table to see if we've seen the group before.# If so, use the data from the table. If not, fetch the group members and add the data to the table.[array]$GroupMembers = $null$GroupId = $GrantedToIdIf ($GroupsHash[$GroupId]) {$GrantedTo = ("Group: {0}:" -f ($GrantedTo))$GrantedToId = $GroupsHash[$GroupId]} Else {[array]$GroupMembers = Get-MgGroupMember -GroupId $GroupId$GrantedTo = ("Group: {0}:" -f ($GrantedTo))$GroupMembersNames = ("Members: {0}" -f ($GroupMembers.additionalProperties.displayName -join ","))$GroupsHash.Add($GroupId, $GroupMembersNames)$GrantedToId = $GroupMembersNames}}If ($Permission.Link.PreventDownload) {$PreventDownload = $true} Else {$PreventDownload = $false}If ($null -ne $GrantedTo) {$ReportLine = [PSCustomObject]@{Account = $Site.DisplayNameItemName = $File.NameFolder = $File.parentreference.name'Access granted to' = $GrantedTo'Effective scope' = $GrantedToIdPermission = $Scope.trim()Size = (FormatFileSize $File.Size)Created = $FileCreatedDateTimeAuthor = $File.CreatedBy.User.DisplayNameHasPassword = $Permission.HasPassword'Expiration date' = $ExpirationDate'Link Expired' = $LinkExpired'Prevents Download' = $PreventDownloadLastModified = $LastModifiedDateTime'Last modified by' = $File.LastModifiedBy.User.DisplayNameItemUrl = $File.WebUrlSiteUrl = $Site.WebUrl}$Report.Add($ReportLine)Start-Sleep -Milliseconds 50}}}}# Process the foldersForEach ($Folder in $Folders) {Write-Host ("Processing folder {0} ({1} files/size {2})" -f $Folder.Name, $Folder.folder.childcount, (FormatFileSize $Folder.Size))Get-DriveItems -Drive $Drive -FolderId $Folder.Id}}function FormatFileSize {# Format File Size nicelyparam ([parameter(Mandatory = $true)]$InFileSize)If ($InFileSize -lt 1KB) { # Format the size of a document$FileSize = $InFileSize.ToString() + " B"}ElseIf ($InFileSize -lt 1MB) {$FileSize = $InFileSize / 1KB$FileSize = ("{0:n2}" -f $FileSize) + " KB"}Elseif ($InFileSize -lt 1GB) {$FileSize = $InFileSize / 1MB$FileSize = ("{0:n2}" -f $FileSize) + " MB"}Elseif ($InFileSize -ge 1GB) {$FileSize = $InFileSize / 1GB$FileSize = ("{0:n2}" -f $FileSize) + " GB"}Return $FileSize}function Test-Numeric ($Value) {return $Value -match "^[\d\.]+$"}function Get-BaseUrl {param ([string]$Url)$LastSlashIndex = $Url.LastIndexOf('/')if ($LastSlashIndex -gt -1) {return $Url.Substring(0, $LastSlashIndex+1)} else {return $Url}}function Convert-UPNToOneDrive {param ([string]$UPN)$UPN.ToLower() -replace '\.', '_' -replace '@', '_' -replace '\.', '_'}# ==== Start of processing ====# Define the values required to connect. These are for the Entra ID app used to authentciate with the Graph# You must replace these values with your own.$Tenantid = 'a662313f-14fc-43a2-9a7a-d2e27f4f3478'# Make sure that the certificate is in date and won't expire soon!$Thumbprint = "2C9529B1FFD08BCD483A5D98807E47A472C5318"Connect-MgGraph -AppId $AppId -TenantId $TenantId -CertificateThumbprint $Thumbprint -NoWelcome# Find SharePoint sites (use Get-MgAllSite to handle multi-geo tenants)Write-Host "Looking for OneDrive for Business sites..."[array]$Sites = Get-MgSite -All -PageSize 500 -Property DisplayName, WebUrl, IsPersonalSite, CreatedByUser, CreatedDateTime, Description, Name, id# Reduce the set to OneDrive sites[array]$OneDriveSites = $Sites | Where-Object {$_.IsPersonalSite -eq $true}If ($OneDriveSites.Count -eq 0) {Write-Host "No OneDrive for Business sites found"Break} Else {Write-Host ("Found {0} OneDrive for Business sites" -f $OneDriveSites.Count)Write-Host "Some of the sites might be for users who have been removed from the tenant. The script will skip these sites during processing."}# Find the base URL for OneDrive sites. It will be something like https://contoso-my.sharepoint.com/personal/$BaseUrl = Get-BaseURL $OneDriveSites[0].WebUrlWrite-Host "Finding user account information to validate OneDrive sites..."# Fetch licensed users so that we can check OneDrive sites to find ones for current users[array]$Users = Get-MgUser -Filter "assignedLicenses/`$count ne 0 and userType eq 'Member'" `-ConsistencyLevel eventual -CountVariable Records -All -PageSize 999 | Sort-Object displayName$UserHash = @{}# The hash table contains an entry for each user with their UPN (value) and the converted OneDrive URL (name).# The URL is used to match the OneDrive site with the user.ForEach ($User in $Users) {$OneDriveURL = $BaseURL + (Convert-UPNToOneDrive -UPN $User.UserPrincipalName)$UserHash.Add($OneDriveURL, $User.UserPrincipalName)}# Set up the report variables$Global:Report = [System.Collections.Generic.List[Object]]::new()$Global:GroupsHash = @{}[int]$i = 0# Go and processForEach ($Site in $OneDriveSites) {If ($UserHash[$Site.WebURL]) {$Global:TotalFiles = 0$i++Write-Host ("Processing OneDrive site for {0} {1}/{2}" -f $Site.DisplayName, $i, $Users.Count) -ForegroundColor YellowTry {[array]$Drives = Get-MgSiteDrive -SiteId $Site.id$Drive = $Drives | Where-Object {$_.Name -like "OneDrive*"}Get-DriveItems -Drive $Drive.Id -FolderId "root"}Catch {Write-Host ("Error processing OneDrive site {0}. The account might be locked or the user might never have used OneDrive." -f $Site.DisplayName)Continue}# Brief pause before we process the next accountStart-Sleep -Seconds 2}}# Build a list of users who share files[array]$UsersWhoShare = $Report | Sort-Object Account -Unique | Select-Object AccountForEach ($U in $UsersWhoShare) {$UPN = $UserHash[$U.Account]If ($UPN) {Add-Member -InputObject $U -MemberType NoteProperty -Name "UPN" -Value $UPN}[int]$NumberFiles = 0[int]$AnyOneLinks = 0$NumberFiles = $Report | Where-Object {$_.Account -eq $U.Account} | Sort-Object ItemName -Unique | Measure-Object | Select-Object -ExpandProperty Count$AnyOneLinks = $Report | Where-Object {$_.Account -eq $U.Account -and $_.'Access granted to' -eq "Anyone link"} | Measure-Object | Select-Object -ExpandProperty Count$OrganizationLinks = $Report | Where-Object {$_.Account -eq $U.Account -and $_.'Access granted to' -eq "Organization link"} | Measure-Object | Select-Object -ExpandProperty CountAdd-Member -InputObject $U -MemberType NoteProperty -Name "Shared Files" -Value $NumberFilesAdd-Member -InputObject $U -MemberType NoteProperty -Name "Anyone Links" -Value $AnyoneLinksAdd-Member -InputObject $U -MemberType NoteProperty -Name "Organization Links" -Value $OrganizationLinks}If (!($UsersWhoShare)) {Write-Host "No sharing activity found in OneDrive for Business sites"Break} Else {Write-Host ("Found {0} users who share files in OneDrive for Business sites" -f $UsersWhoShare.Count)Write-Host ""}Write-Host "Summary of sharing activity in OneDrive for Business sites"$UsersWhoShare | Format-table -AutoSizeWrite-Host ""If (Get-Module ImportExcel -ListAvailable) {$ExcelGenerated = $TrueImport-Module ImportExcel -ErrorAction SilentlyContinue$SummaryOutputFile = ((New-Object -ComObject Shell.Application).Namespace('shell:Downloads').Self.Path) + "\OneDrive Files Sharing Summary Report.xlsx"If (Get-Item $SummaryOutputFile -ErrorAction SilentlyContinue) {Remove-Item $SummaryOutputFile -ErrorAction SilentlyContinue}$UsersWhoShare | Export-Excel -Path $SummaryOutputFile -WorksheetName "OneDrive Files Sharing Summary" `-Title "OneDrive Files Sharing Summary Report" -TitleBold -TableName "OneDriveSharingSummary"$DetailedOutputFile = ((New-Object -ComObject Shell.Application).Namespace('shell:Downloads').Self.Path) + "\OneDrive Files Sharing Report.xlsx"If (Get-Item $DetailedOutputFile -ErrorAction SilentlyContinue) {Remove-Item $DetailedOutputFile -ErrorAction SilentlyContinue}$Report | Export-Excel -Path $DetailedOutputFile -WorksheetName "OneDrive Files Sharing Details" `-Title "OneDrive for Business Files Sharing Details" -TitleBold -TableName "OneDriveSharingDetails"} Else {$SummaryCSVOutputFile = ((New-Object -ComObject Shell.Application).Namespace('shell:Downloads').Self.Path) + "\OneDrive Files Sharing Summary Report.CSV"$UsersWhoShare | Export-Csv -Path $SummaryCSVOutputFile -NoTypeInformation -Encoding Utf8$DetailedCSVOutputFile = ((New-Object -ComObject Shell.Application).Namespace('shell:Downloads').Self.Path) + "\OneDrive Files Sharing Detailed Report.CSV"$Report | Export-Csv -Path $DetailedCSVOutputFile -NoTypeInformation -Encoding Utf8}If ($ExcelGenerated) {Write-Host ("Excel worksheets generated in your Downloads folder: {0}, {1}" -f $SummaryOutputFile, $DetailedOutputFile)} Else {Write-Host ("CSV files generated in your Downloads folder: {0}, {1}" -f $SummaryCSVOutputFile, $DetailedCSVOutputFile)}
Parameters
ParameterDefaultNotes
-TenantId""Microsoft Entra tenant ID for app-only Graph authentication.-AppId""Application (client) ID for the app registration used to connect.Attribution
Author
Office365itpros