Skip to content
Discover 6 min read

How to find every SharePoint site with broken permission inheritance

The admin center will not tell you, unless you pay for SharePoint Advanced Management, and even then the report you want does not exist. Here is the check that works by hand, and the script that covers a whole tenant in two logins.

On this page

What hurts

Broken permission inheritance is where oversharing starts. A library gets unique permissions during some 2021 project, someone is added directly instead of through a group, and it stays that way. Nothing in SharePoint tells you it happened, and every migration, permissions cleanup and Copilot rollout has to find it first.

The obvious place to look does not have the answer.

The admin center will not tell you

Data access governance reports look like they should cover this. They do not, for two separate reasons.

SharePoint admin center · Reports, Data access governance

Two reports, neither about inheritance. The licensed reports are not greyed out here, they simply are not present.

The first reason is licensing. Data access governance is a SharePoint Advanced Management capability, so it needs SAM or Microsoft 365 E5. Business Premium tenants get the two reports above and nothing else. As of the 2026 SAM expansion there’s also a separate SharePoint Advanced Management Administrator role in Entra, so a full SharePoint Admin without that role sees the reports as inaccessible even where the licence exists.

The second reason matters more: even with SAM, there is no broken-inheritance report. The reports cover sharing links, sensitivity labels and oversharing by “Everyone except external users.” Inheritance is not among them. Buying the licence to answer this specific question does not answer it.

Checking one list by hand

For a single site this takes about fifteen seconds and costs nothing.

Go to the list or library, then Settings → List settings → Permissions for this list. Look for the yellow banner.

List settings · Permissions for this list

The banner is the giveaway, and the ribbon confirms it with a Delete unique permissions button. An inheriting list shows neither.

The banner reads This list has unique permissions, and the ribbon offers Delete unique permissions under Inheritance. A list still inheriting from its site shows neither, and offers Stop Inheriting Permissions instead.

That’s fine for a handful of sites. It does not scale to a tenant, and there is no view anywhere that lists every broken-inheritance location at once.

Scripting the whole tenant

PnP PowerShell answers it properly. Three prerequisites bite first, and all three break scripts you’ll find published elsewhere.

Once registered, the scan needs two device logins total: one for the tenant admin connection, one for the first site. Every site after that reuses the cached token silently, so an eight-site tenant asks twice, not eight times.

$ErrorActionPreference = 'Stop'
$clientId  = "<your-app-client-id>"
$adminUrl  = "https://<tenant>-admin.sharepoint.com"
$csvPath   = "$env:USERPROFILE\Desktop\broken-inheritance.csv"
$itemLimit = 500   # lists bigger than this are not item-scanned, and are reported

Connect-PnPOnline -Url $adminUrl -ClientId $clientId -DeviceLogin
if (-not (Get-PnPConnection)) { throw "Not connected. Aborting." }

$sites = Get-PnPTenantSite -Filter "Url -like 'sharepoint.com/sites'"
if ($sites.Count -eq 0) { throw "Zero sites returned. That is an auth problem, not an empty tenant." }

$findings = @(); $notScanned = @()
foreach ($site in $sites) {
    $short = $site.Url -replace 'https://[^/]+',''
    try {
        Connect-PnPOnline -Url $site.Url -ClientId $clientId -DeviceLogin
        foreach ($l in (Get-PnPList -Includes HasUniqueRoleAssignments | Where-Object { -not $_.Hidden })) {

            if ($l.HasUniqueRoleAssignments) {
                $findings += [pscustomobject]@{
                    Site = $short; Container = $l.Title; Scope = 'List'; Name = ''; Items = $l.ItemCount
                }
            }

            # Item level. This is the part a list-only scan misses completely.
            if ($l.ItemCount -gt $itemLimit) {
                $notScanned += [pscustomobject]@{ Site = $short; List = $l.Title; Items = $l.ItemCount }
                continue
            }
            foreach ($it in (Get-PnPListItem -List $l -PageSize 200)) {
                if (Get-PnPProperty -ClientObject $it -Property HasUniqueRoleAssignments) {
                    # Title for list items, FileLeafRef for documents.
                    $label = $it.FieldValues.Title
                    if (-not $label) { $label = $it.FieldValues.FileLeafRef }
                    $findings += [pscustomobject]@{
                        Site = $short; Container = $l.Title; Scope = 'Item'; Name = $label; Items = 1
                    }
                }
            }
        }
    } catch {
        Write-Host ("  SKIPPED {0}" -f $short) -ForegroundColor Yellow
    }
}

$findings | Export-Csv -Path $csvPath -NoTypeInformation -Encoding UTF8
$findings | Group-Object Scope | ForEach-Object { Write-Host ("{0,-5} {1}" -f $_.Name, $_.Count) }
if ($notScanned.Count -gt 0) {
    Write-Host "NOT item-scanned, too large:" -ForegroundColor Yellow
    $notScanned | Format-Table -AutoSize
}

Output from a real run, with a deliberately planted item-level permission so there was something to catch:

Connected. Scanning 8 site collections...

Site                  Container         Scope Name          Items
----                  ---------         ----- ----          -----
/sites/SampleTeamSite Equipment Request List                    1
/sites/SampleTeamSite Equipment Request Item  ZZ-perm-test      1

  Item  1
  List  1
CSV written to C:\Users\...\Desktop\broken-inheritance.csv

The Scope column is what makes the CSV worth handing to someone. List rows are containers with unique permissions. Item rows are individual files or list items, and they’re the ones nobody knows about.

Why the item scan has a size limit

Checking item-level permissions costs one API call per item. There’s no bulk property to ask for. A 20,000-item library means 20,000 calls, which will run for hours and get you throttled.

So the script skips lists over 500 items and prints what it skipped. That number is arbitrary; raise it if you have time and patience.

What matters is that the skip is visible. A scan that quietly ignores your biggest libraries, which are exactly the ones most likely to have item-level sprawl, is worse than no scan, because it produces a clean report you’ll believe.

Fixing what you find

Re-inheriting a list is one line:

Connect-PnPOnline -Url "https://<tenant>.sharepoint.com/sites/<site>" -ClientId $clientId -DeviceLogin
Set-PnPList -Identity "Equipment Request" -ResetRoleInheritance

Run it per site, interactively, and check the result. Do not put this in the scan loop. The scan is read-only and safe to run against a whole tenant; resetting inheritance is destructive and removes every permission that was granted directly on that list. Someone relying on one of those grants loses access the moment it runs.

Which is why the answer is not “reset everything you found.”

The guard clause is not optional

Note the two throw statements. They exist because of what happened the first time I ran this.

The device code expired before I entered it. The connection failed, Get-PnPTenantSite returned nothing, the loop had nothing to iterate, and the script printed “No broken inheritance found” and exited with code 0. A clean bill of health from a script that never authenticated.

Most published versions of this script have no such guard. If you copy one, add the site-count check before you trust a green result. A scan that finds nothing and a scan that never ran look identical in the output, and only one of them is good news.

What to do with the list

Broken inheritance is not automatically wrong. A genuinely restricted HR library should have unique permissions. What you’re looking for is the accidental kind: a list nobody meant to break, usually with individual users granted directly instead of through a group.

Work the CSV in this order:

  1. Item rows first. Individual files with unique permissions are almost always accidental, created by someone using Share on a single document years ago. Nobody is maintaining them.
  2. List rows with a named owner. Ask the owner whether the restriction is still needed. Half the time it protects a project that finished in 2022.
  3. List rows with no owner. These are the ones to reset, after a notice period.
  4. Anything you skipped for size. Those libraries still need an answer, just not from this script.

An empty list from an abandoned project is worth deleting outright rather than fixing.

Then run the full permissions audit before you migrate anything, because inheritance is one of five things that go wrong and the others do not show up in this scan.

Bottom line

The admin center cannot answer this question at any licence tier. The by-hand check is fifteen seconds per list and the scripted one covers a tenant in two logins, once you have PowerShell 7 and your own Entra app.

Add the guard clause. The failure mode of this script is a false all-clear, and that is the one result nobody double-checks.

Paired with this post

Permissions Cleanup & Governance Prep Checklist

PDF · 7 pages · 45 checkpoints · one email, no drip sequence

One email with the link. No drip sequence, no upsell. Unsubscribe any time.

TWENTY MINUTES, NO PITCH

Tell me what is stuck. I will tell you what it takes.

Same consultant from the first email to the last cutover. If I am not the right fit, I will refer you to someone who is.

Sneak peek

Document preview

100%

Loading the document…