Compliance & Auditintermediate

How to Investigate Who Changed SharePoint Permissions

A step-by-step method for attributing a SharePoint Online permission change to an actor and a timestamp, reading the audit record, and reconstructing the before and after state.

PTAccess Witness Product TeamProduct and engineeringPublished Updated 10 min read

A finance folder that should have been restricted to five people now shows a contractor group with Edit. Nobody admits to changing it. The question is not "who has access" but "who granted it, when, and what did it look like before." This guide is the procedure for answering that from the Microsoft 365 Unified Audit Log, with the pitfalls marked.

What the audit log records for permissions

SharePoint permission changes are logged under the SharePoint workload with operation names that describe the mechanism, not the intent. The ones you will use most:

OperationWhat happened
SharingInheritanceBrokenAn object stopped inheriting and got its own role assignments
SharingInheritanceResetAn object went back to inheriting
PermissionLevelAddedA principal was bound to a permission level on an object
PermissionLevelRemovedA binding was removed
PermissionLevelModifiedA binding changed (for example Read to Edit)
AddedToGroup / RemovedFromGroupA SharePoint group's membership changed
GroupAdded / GroupRemovedA SharePoint group was created or deleted
SharingSetSharing was configured on an item (often paired with a link event)
SharingLinkCreated, SecureLinkCreated, AnonymousLinkCreatedA sharing link was created
AddedToSecureLink / RemovedFromSecureLinkA specific-people link's audience changed
SharingRevoked, SharingLinkDisabled, AnonymousLinkRemovedSharing was removed
SiteCollectionAdminAdded / SiteCollectionAdminRemovedSite collection administrators changed

Entra ID group membership changes are logged under the Azure Active Directory workload as Add member to group. and Remove member from group., with the trailing period. They are just as relevant: adding someone to a security group that already sits in a SharePoint group is a permission change that never appears as a SharePoint operation.

Each record has a CreationDate (UTC), UserIds (the actor), Operations, and an AuditData JSON blob with the specifics.

Start narrow: the object, a time window, and the relevant operations. Using Exchange Online PowerShell:

Connect-ExchangeOnline

$objectUrl = "https://contoso.sharepoint.com/sites/Finance/Shared Documents/Payroll"
$start = [datetime]::Parse("2026-01-01T00:00:00Z").ToUniversalTime()
$end   = [datetime]::Parse("2026-02-01T00:00:00Z").ToUniversalTime()

$results = Search-UnifiedAuditLog -StartDate $start -EndDate $end `
  -RecordType SharePoint `
  -Operations SharingInheritanceBroken,PermissionLevelAdded,PermissionLevelRemoved,PermissionLevelModified,AddedToGroup,RemovedFromGroup `
  -ObjectIds $objectUrl -ResultSize 5000

-ObjectIds matches the resource URL exactly, so a change on a file inside the folder will not match the folder URL. For a subtree, drop -ObjectIds, use -SiteIds (the site collection GUID) and filter on ObjectId afterwards:

$results | ForEach-Object {
  $d = $_.AuditData | ConvertFrom-Json
  [pscustomobject]@{
    WhenUtc   = $_.CreationDate.ToUniversalTime()
    Actor     = $_.UserIds
    Operation = $_.Operations
    Object    = $d.ObjectId
    Target    = $d.TargetUserOrGroupName
    Level     = $d.EventData
  }
} | Where-Object { $_.Object -like "$objectUrl*" } | Sort-Object WhenUtc

If the window is wide or the site is busy, use -SessionId with -SessionCommand ReturnLargeSet and page until a batch returns fewer than 5000 rows. Never assume one call returned everything.

Step 2: Read the record

Take the PermissionLevelModified event from the Contoso Finance sample scenario as an example. The parsed fields look like this:

WhenUtc   : 2026-01-20 15:05:41
Actor     : jordane@contoso.onmicrosoft.com
Operation : PermissionLevelModified
Object    : https://contoso.sharepoint.com/sites/Finance/Shared Documents/Payroll
Target    : Finance Visitors
Level     : <PermissionLevel>Edit</PermissionLevel><PreviousPermissionLevel>Read</PreviousPermissionLevel>

Three facts are established: who (the actor's UPN), when (UTC, to the second), and what (Finance Visitors on Payroll went from Read to Edit). What the record does not tell you is why, or who was affected downstream.

Some records need a second look:

  • SharingInheritanceBroken logs the object that became unique. The role assignments it starts with are a copy of the parent's, but the copy is not itself logged as PermissionLevelAdded. To know the initial state you need the parent's assignments at that moment.
  • App-only actors appear as app@sharepoint or the application's display name. That means an automation, a provisioning engine or a governance tool made the change under an application identity; the human trigger, if any, is in that system's own logs.
  • SharingSet frequently accompanies link events and can look like a duplicate. It is the "sharing was configured" umbrella; the link event carries the audience.

Step 3: Establish the before and after

The audit record gives a delta. To state the before and after honestly you need the state on either side of it. There are two sources:

  1. Your own baselines, if you keep them (see How to Audit Historical SharePoint Permissions). Take the nearest baseline before the event, replay events up to it, and you have "before." Apply the event and you have "after."
  2. Adjacent events, if you do not. PermissionLevelModified carries PreviousPermissionLevel, which is a gift. PermissionLevelAdded implies the principal was absent before; PermissionLevelRemoved implies it was present. Chain them and you can reconstruct the binding's history for that principal, but not the whole object's state.

For the Payroll example, chaining gives: unique since 12 December (Robert Williams broke inheritance, then removed Finance Members), Visitors elevated to Edit on 20 January (Jordan Ellis), reverted to Read on 28 January (Jordan Ellis). The exposure window is eight days.

Step 4: Find who was affected

A permission change on a group affects every member of that group, transitively. Take the target principal from the record and expand it as of the event time, not as of today. Finance Visitors contained the Entra ID security group Finance Contractors, which contained the guest Alex Turner and the nested group External Auditors. Every one of those identities could edit payroll files for eight days.

If you only have current membership, say so in the finding. "Members of Finance Visitors as of today" is a different claim from "members of Finance Visitors on 20 January."

Investigations rarely involve one event. Widen the window around the timestamp and look for:

  • Other permission changes by the same actor in the same session (batch changes often come in bursts seconds apart).
  • Group membership changes on the target group around the same time (someone added to the group the day before it was elevated is a pattern worth noting).
  • FileAccessed, FileDownloaded and FileSyncDownloadedFull events on the object during the exposure window by identities that only had access because of the change. That is the difference between "could have" and "did."
Search-UnifiedAuditLog -StartDate "2026-01-20T15:00:00Z" -EndDate "2026-01-28T10:00:00Z" `
  -RecordType SharePointFileOperation -Operations FileAccessed,FileDownloaded `
  -SiteIds $financeSiteId -ResultSize 5000 |
  ForEach-Object { $d = $_.AuditData | ConvertFrom-Json; $d } |
  Where-Object { $_.ObjectId -like "*/Payroll/*" } |
  Select-Object CreationTime, UserId, Operation, ObjectId

Step 6: Write it up

A defensible finding contains:

  • The object (URL and id), the change, the actor, the UTC timestamp, and the raw audit record id.
  • The state before and after, with the source of that state (baseline, chained events, or current state with a caveat).
  • The affected identities, expanded as of the event time, with the method used.
  • Related events in the window.
  • The confidence of each element, including any retention or coverage limits.

Keep the raw records. Parsed summaries are for reading; raw AuditData is for proving.

FAQ

The actor is shown as a GUID or as app@sharepoint. Who is that?
An application identity made the change. Look up the GUID in Entra ID enterprise applications. The human who triggered the automation, if any, is only in that application's own logs.
Why is there no event for the permissions copied when inheritance was broken?
SharingInheritanceBroken records the break itself. The initial unique assignments are a copy of the parent and are not individually logged, so the parent's state at that time is the only way to know them.
Can I get these events without Exchange Online PowerShell?
Yes. The Microsoft Purview audit search in the compliance portal exposes the same log, and the Microsoft Graph audit log query API can run the query programmatically with an application identity.
auditinvestigationssharepoint-onlineunified-audit-log
All guides
  • Historical Access12 min read, advanced

    How to Audit Historical SharePoint Permissions

    SharePoint keeps no permission history. This guide shows what the audit log can and cannot reconstruct, how to build a baseline plus event approach with PowerShell, and how to state confidence honestly.

  • Microsoft 365 Security9 min read, beginner

    How Long Does Microsoft 365 Keep SharePoint Audit History?

    Retention periods for the Unified Audit Log under Audit (Standard) and Audit (Premium), what the 10-year add-on covers, how audit delay works, and why none of it is a permission history.

  • SharePoint Permissions11 min read, intermediate

    How to Find Out Why a User Has Access to a SharePoint Site

    Trace direct, inherited and nested group access in SharePoint Online, from the role assignment on the resource down to the identity, using the admin UI, REST and PowerShell.