SharePoint/OneDrive External Sharing Auditor
Shared Workflow Routing
- Use the shared workflow spec for deterministic multi-plugin routing:
workflows/multi-plugin-workflows.md.
- Apply the trigger phrases, handoff contracts, auth prerequisites, validation checkpoints, and stop conditions before escalating to the next plugin.
This skill provides comprehensive knowledge for auditing and managing external sharing across SharePoint and OneDrive via Graph API and SharePoint PowerShell, with a focus on safe remediation that avoids accidental data loss.
Integration Context Contract
| Workflow |
tenantId |
subscriptionId |
environmentCloud |
principalType |
scopesOrRoles |
| Sharing scan and remediation |
required |
optional |
AzureCloud* |
delegated-user |
Sites.Read.All, Sites.FullControl.All, User.Read.All, AuditLog.Read.All |
* Use sovereign cloud values from the canonical contract when applicable.
Fail fast before Graph/SharePoint calls when required context is missing or invalid. Redact tenant/object identifiers in outputs.
Base URL
https://graph.microsoft.com/v1.0
All Graph API endpoints below are relative to this base URL.
API Endpoints
Drive Item Permissions
| Method |
Endpoint |
Purpose |
| GET |
/sites/{siteId}/drives/{driveId}/items/{itemId}/permissions |
List sharing permissions on a file/folder |
| DELETE |
/sites/{siteId}/drives/{driveId}/items/{itemId}/permissions/{permId} |
Remove a sharing permission |
| PATCH |
/sites/{siteId}/drives/{driveId}/items/{itemId}/permissions/{permId} |
Update a permission (e.g., change role) |
| POST |
/sites/{siteId}/drives/{driveId}/items/{itemId}/invite |
Create sharing invitation |
Guest User Management
| Method |
Endpoint |
Purpose |
| GET |
/users?$filter=userType eq 'Guest' |
List all guest users |
| GET |
/users/{guestId} |
Get guest user details |
| DELETE |
/users/{guestId} |
Remove guest user |
| GET |
/users/{guestId}/memberOf |
List guest's group memberships |
Site Sharing Configuration
| Method |
Endpoint |
Purpose |
| GET |
/sites/{siteId} |
Get site details including sharing settings |
| GET |
/sites?search={keyword} |
Search for sites |
| GET |
/sites/{siteId}/drives |
List document libraries |
| GET |
/sites/{siteId}/permissions |
List site-level permissions |
Access Reviews (Beta)
| Method |
Endpoint |
Purpose |
| POST |
/beta/identityGovernance/accessReviews/definitions |
Create access review |
| GET |
/beta/identityGovernance/accessReviews/definitions |
List access reviews |
| GET |
/beta/identityGovernance/accessReviews/definitions/{id}/instances/{id}/decisions |
Get review decisions |
Sharing Link Types
| Type |
link.scope Value |
Risk Level |
Description |
| Anonymous |
anonymous |
High |
Anyone with the link, no sign-in required |
| Organization |
organization |
Low |
Anyone in the tenant |
| Specific people (internal) |
users |
Low |
Named internal users only |
| Specific people (external) |
users |
Medium |
Named external guests |
| Company-wide + edit |
organization |
Medium |
Everyone in org with edit rights |
Permission Link Response Example
{
"id": "perm-id-abc123",
"roles": ["read"],
"link": {
"scope": "anonymous",
"type": "view",
"webUrl": "https://contoso.sharepoint.com/:b:/s/project/abc123",
"preventsDownload": false
},
"grantedToIdentitiesV2": [],
"hasPassword": false,
"expirationDateTime": null,
"createdDateTime": "2025-11-15T08:30:00Z"
}
Key risk indicators in permission objects:
link.scope = anonymous — anyone can access without sign-in
expirationDateTime = null — link never expires
hasPassword = false — no password protection
roles includes write or owner — edit access to external parties
SharePoint Tenant Sharing Policies
SharingCapability Values
| Value |
Description |
Risk Level |
Disabled |
No external sharing allowed |
None |
ExistingExternalUserSharingOnly |
Only existing guest accounts |
Low |
ExternalUserSharingOnly |
New and existing guests (no anonymous links) |
Medium |
ExternalUserAndGuestSharing |
Anyone, including anonymous links |
High |
SharePoint PowerShell Commands
# Connect to SharePoint admin
Connect-SPOService -Url "https://contoso-admin.sharepoint.com"
# Get tenant-level sharing settings
Get-SPOTenant | Select-Object SharingCapability, DefaultSharingLinkType,
DefaultLinkPermission, RequireAnonymousLinksExpireInDays,
ExternalUserExpirationRequired, ExternalUserExpireInDays
# Get site-level sharing settings
Get-SPOSite -Identity "https://contoso.sharepoint.com/sites/project" |
Select-Object SharingCapability, SharingAllowedDomainList,
SharingBlockedDomainList, DefaultSharingLinkType
# List external users for a site
Get-SPOExternalUser -SiteUrl "https://contoso.sharepoint.com/sites/project"
Tenant Policy Enum Reference
| Setting |
Values |
Description |
DefaultSharingLinkType |
None, Direct, Internal, AnonymousAccess |
Default link type when sharing |
DefaultLinkPermission |
None, View, Edit |
Default permission for new links |
FileAnonymousLinkType |
View, Edit |
Anonymous link permission for files |
FolderAnonymousLinkType |
View, Edit |
Anonymous link permission for folders |
RequireAnonymousLinksExpireInDays |
Integer (0 = no expiry) |
Anonymous link expiry |
ExternalUserExpireInDays |
Integer |
Guest access expiry |
Guest User Health Checks
Stale Guest Detection (90+ Days Inactive)
GET /users?$filter=userType eq 'Guest' and signInActivity/lastSignInDateTime le 2025-12-01T00:00:00Z
&$select=displayName,mail,userPrincipalName,signInActivity,createdDateTime,externalUserState
&$count=true
&$orderby=signInActivity/lastSignInDateTime asc
Header: ConsistencyLevel: eventual
Guest Accounts Pending Redemption
GET /users?$filter=userType eq 'Guest' and externalUserState eq 'PendingAcceptance'
&$select=displayName,mail,createdDateTime,externalUserState
&$count=true
Header: ConsistencyLevel: eventual
External User State Values
| State |
Description |
PendingAcceptance |
Invitation sent but not redeemed |
Accepted |
Guest has redeemed invitation |
Pagination Pattern
All list endpoints may return paginated results. Follow @odata.nextLink to fetch additional pages:
let allResults = [];
let url = "/users?$filter=userType eq 'Guest'&$top=100";
while (url) {
const response = await client.api(url).get();
allResults.push(...response.value);
url = response["@odata.nextLink"] || null;
}
Important: Always paginate when scanning guest users or permissions — tenants may have thousands of guests.
Safe Remediation Principles
- Never hard-delete sharing links without owner approval
- Generate approval tasks instead of immediate revocation
- Notify the file owner before any link is removed
- Provide context — show what the link grants access to and who has used it
- Batch by owner — group findings by file owner for efficient review
- Snapshot before change — record the permission object before deletion
Revocation Patterns
Remove a Sharing Permission
DELETE /sites/{siteId}/drives/{driveId}/items/{itemId}/permissions/{permissionId}
Remove a Guest User
DELETE /users/{guestUserId}
Disable Anonymous Links at Site Level
Set-SPOSite -Identity "https://contoso.sharepoint.com/sites/project" -SharingCapability ExternalUserSharingOnly
Set Link Expiration Policy
Set-SPOTenant -RequireAnonymousLinksExpireInDays 30
Required Permissions
| Operation |
Permission / Role |
| Read sharing links |
Sites.Read.All or Sites.ReadWrite.All |
| Remove permissions |
Sites.ReadWrite.All |
| List guest users |
User.Read.All |
| Read sign-in activity |
AuditLog.Read.All + User.Read.All |
| Delete guest users |
User.ReadWrite.All |
| SharePoint admin settings |
SharePoint Administrator role |
| Access reviews |
AccessReview.ReadWrite.All |
Error Handling
| Status Code |
Meaning |
Common Cause |
| 400 Bad Request |
Malformed request |
Invalid OData filter syntax, bad $filter on signInActivity |
| 401 Unauthorized |
Authentication failure |
Expired token, missing Authorization header |
| 403 Forbidden |
Insufficient permissions |
App lacks Sites.Read.All or SharePoint Admin role |
| 404 Not Found |
Resource not found |
Invalid site ID, deleted file, removed guest |
| 409 Conflict |
Operation conflict |
Guest user has active access review in progress |
| 429 Too Many Requests |
Throttled |
Implement exponential backoff with Retry-After header |
ConsistencyLevel Requirement
Queries using $count, $search, or advanced filters on signInActivity require:
Header: ConsistencyLevel: eventual
Without this header, the API returns 400 Bad Request.
Common Audit Patterns
Pattern 1: Full External Sharing Audit
Get-SPOTenant — check tenant-level sharing policy
Get-SPOSite -Limit All — list all sites with their sharing capability
- For each site with
ExternalUserAndGuestSharing: scan GET .../permissions for anonymous links
GET /users?$filter=userType eq 'Guest' — enumerate all guest accounts
- Cross-reference guests with site permissions to identify orphaned access
- Produce report grouped by risk level (High: anonymous, Medium: guest, Low: internal)
Pattern 2: Stale Guest Cleanup
GET /users?$filter=userType eq 'Guest' and signInActivity/lastSignInDateTime le {90daysAgo} — find stale guests
- For each stale guest:
GET /users/{id}/memberOf — list group memberships
- Generate approval tasks for guest removal, grouped by the internal sponsor
- After approval:
DELETE /users/{guestId} — remove guest accounts
- Verify removal: re-run guest query to confirm count reduction
Pattern 3: Anonymous Link Remediation
- Identify sites with
SharingCapability = ExternalUserAndGuestSharing
- Scan all drive items for permissions where
link.scope = 'anonymous'
- Flag links with no expiration (
expirationDateTime = null)
- Generate owner notification with link details and access history
- After owner review:
DELETE .../permissions/{id} to remove approved links
Set-SPOSite -SharingCapability ExternalUserSharingOnly — downgrade site policy
Pattern 4: Tenant Sharing Policy Hardening
- Audit current tenant settings:
Get-SPOTenant
- Set anonymous link expiration:
Set-SPOTenant -RequireAnonymousLinksExpireInDays 30
- Set guest expiration:
Set-SPOTenant -ExternalUserExpirationRequired $true -ExternalUserExpireInDays 90
- Set default link type to internal:
Set-SPOTenant -DefaultSharingLinkType Internal
- Block specific external domains:
Set-SPOTenant -SharingBlockedDomainList "competitor.com"
- Verify changes and document in change log
OData Filter Reference
| Filter |
Purpose |
Example |
| Guest users |
Find all guests |
$filter=userType eq 'Guest' |
| Stale guests |
Inactive 90+ days |
$filter=userType eq 'Guest' and signInActivity/lastSignInDateTime le 2025-12-01 |
| Pending guests |
Never redeemed |
$filter=userType eq 'Guest' and externalUserState eq 'PendingAcceptance' |
| Sort by activity |
Least active first |
$orderby=signInActivity/lastSignInDateTime asc |
| With count |
Include total count |
$count=true (requires ConsistencyLevel: eventual) |
Progressive Disclosure — Reference Files
| Topic |
File |
| Office 365 Management Activity API, SharePoint audit events, Graph audit log queries, Purview audit search, report scheduling |
references/audit-reporting.md |
| Graph driveItem permissions API, anonymous/org-wide link detection, external sharing by site, scanning at scale |
references/overshared-links.md |
| Guest sign-in activity API, stale guest detection, access reviews, B2B collaboration settings, guest expiration |
references/stale-guests.md |
| Bulk permission removal, anonymous→specific link conversion, guest removal, tenant-level prevention settings, CA for guests |
references/remediation.md |