TL;DR: You can audit external shares via Microsoft Graph permissions endpoints, but a basic crawler will run into three sharp edges: permissions can’t be expanded across collections,
inheritedFromisn’t populated for SharePoint libraries, and non-owner callers only receive their own permissions (returning an empty report with a200 OKinstead of an error).
If you ask the average admin how to see who outside the company has access to a site, they point to the Admin Center or the “Shared with external users” view under Site Usage. The problem is that neither gives you an actionable list. They’re decent high-level snapshots, but they don’t cleanly export file paths alongside external recipient emails. If you need a spreadsheet where someone can go row-by-row and revoke access, the built-in tooling falls flat.
I put together a small CLI tool to build these remediation lists: heyjudeuk/sharepoint-external-share-audit (MIT). Here is what the script is doing under the hood, along with the Graph quirks you need to consider.
The Basic Logic
Every file’s permissions live at GET /drives/{driveId}/items/{itemId}/permissions. When you query that endpoint, you get back two distinct formats:
- Sharing links: Handled via the
linkobject. Scopes areanonymous(public “anyone” links),organization(internal only), orusers(specific people). - Direct grants: Handled via
grantedToV2orgrantedToIdentitiesV2. If the recipient was invited but hasn’t logged in yet, there’s aninvitationfacet attached.
Filtering is straightforward: ignore organization links, flag anonymous links immediately, and for everything else, cross-reference the recipient’s email domain against the tenant’s verified domains pulled from /organization.
Pulling that data at scale is where things get messy.
Quirk 1: You can’t expand permissions on a collection
You cannot append $expand=permissions to a drive item collection query. Graph outright rejects expanding that relationship when listing files.
If a site library has 12,000 files, you have to enumerate the tree and then make 12,000 individual permission calls. You will get throttled. Your script needs to respect the Retry-After header on HTTP 429 responses, sleep accordingly, and ideally keep a small baseline delay between requests. A scan across a big document library will take several minutes. That’s just the baseline cost of Graph’s API design here.
Quirk 2: inheritedFrom is missing on SharePoint files
The obvious way to optimize 12,000 API calls is to skip any file inheriting its permissions from a folder.
Graph’s permission resource defines an inheritedFrom property for this exact use case. However, the documentation notes a major exception: OneDrive for Business and SharePoint libraries do not populate inheritedFrom. It simply returns null.
You can inspect broken inheritance via the older SharePoint REST API (_api/web/lists/GetByTitle('X')/RoleAssignments), but that requires a separate authentication path and extra complexity. Unless you’re running scans constantly across massive libraries, it’s generally simpler to accept the slower, brute-force item crawl.
Quirk 3: Non-owners get silent 200 OKs with missing data
This is the nastiest trap in the API:
- If the caller is an owner, Graph returns all permissions on the item.
- If the caller is a non-owner, Graph returns only the permissions that apply to that specific caller.
If an admin runs the script with Read or Edit permissions, the API returns a 200 OK for every single file. The crawler finishes without errors, finds zero external shares, and outputs a clean report. The scan didn’t audit the site; it audited the caller’s visibility.
To avoid dangerous false negatives, the script enforces pre-flight checks:
- Requires Site Collection Admin (SCA): The tool checks
IsSiteAdminvia_api/web/currentuserbefore touching the drive. This call targets SharePoint REST rather than Graph, requiring the delegatedAllSites.Readpermission under SharePoint Online. - Avoids
effectiveBasePermissions: While checking base permissions would theoretically let regular Site Owners run scans, the endpoint is notoriously buggy when queried via OAuth bearer tokens. An unreliable permission check is worse than none. - The
--allow-partialescape hatch: If a regular Site Owner explicitly chooses to run the scan anyway, this flag stamps the output file and report header as incomplete.
Why Delegated Auth Over App Permissions?
You could bypass the ownership trap by registering an app with Sites.FullControl.All application permissions.
The downside is handing a single script tenant-wide full control across every SharePoint site. That creates a high-privilege credential you have to store and rotate. For an audit tool run periodically against specific sites, interactive delegated auth scoped to a Site Collection Admin is a much safer security boundary.