Sometimes the Graph Comes Up Short and PnP PowerShell Wins
Sean McAvinue recently wrote about managing SharePoint Online with PnP PowerShell. I’ll be the first to admit that I don’t reach for PnP PowerShell when the time comes to automate SharePoint Online operations. My preference is to use the SharePoint Online management module (recently upgraded to use modern authentication) to manage basic site and tenant settings and the Microsoft Graph PowerShell SDK for anything heavier, like creating reports of files stored in SharePoint Online or a sharing report for OneDrive for Business.
However, sometimes PnP PowerShell is the right choice, usually when the Graph APIs for SharePoint Online are buggy or missing functionality. Regretfully, Microsoft has work to do to improve the SharePoint Graph APIs. The settings API is limited and doesn’t appear to have made any progress since its debut in 2022. The Lists API is buggy and doesn’t return all the information available for lists. It’s very frustrating to see a major Microsoft 365 workload being so badly treated by the Graph.
Default Sensitivity Labels for Document Libraries
Which brings me back to PnP PowerShell. Because it’s a community-driven project, the module is under constant development and enhancements roll out much faster than occurs with any Microsoft software.
My need was simple (or so it seemed on the surface). I wanted to find the set of document libraries in sites that have default sensitivity labels. Assigning a default sensitivity label is a good thing because it means that any file created in the library automatically receives a sensitivity label. It’s a bad thing in another way because once something happens automatically, it’s an excuse for Microsoft to up the licensing requirement. In this instance, default sensitivity labels require every user who accesses the site to have Office 365 E5 licenses.
SharePoint is Built on Lists
The default sensitivity label for a document library is configured as a setting of that library. The first issue is to discover where the setting is stored. SharePoint Online is an app that’s built from lists. The contents of a document library are organized in a list and the settings of that list are where to look (the clue is from the original LinkedIn post about default sensitivity labels).
Conceptually, the programming task is simple. Find all sites, find the document libraries in the sites, and check each list to discover if a default sensitivity label is configured and report what’s found.
It’s certainly possible to search for all sites using the Microsoft Graph PowerShell SDK and then find the document libraries for each site. Application permissions are necessary as otherwise any attempt to retrieve all sites will be limited to whatever sites the signed-in account is a member of. This code fetches all sites, filters to remove personal OneDrive accounts (why a filter isn’t possible with Get-MgSite is unknown), and fetches the document library lists for each site.
[array]$Sites = Get-MgSite -All -PageSize 500 $Sites = $Sites | Where-Object {$_.IsPersonalSite -eq $false} ForEach ($Site in $Sites) { [array]$Lists = Get-MgSiteList -SiteId $Site.Id -All -PageSize 500 $Lists = $Lists | Where-Object {$_.List.Template -eq 'documentLibrary' -and $_.DisplayName -ne 'Teams Wiki Data'} ForEach ($List in $Lists) { # Check the list settings for a default sensitivity label... } }
Then we ran out of town because the Get-MgSiteList cmdlet doesn’t return all the settings for a list. Specifically, it doesn’t return the DefaultSensitivityLabelForLibrary setting, which is where the LinkedIn article told us to look.
I’m sure that the reason is that the Graph API metadata doesn’t include a reference to the DefaultSensitivityLabelForLibrary setting, and without a clue in metadata, the AutoRest process that creates the cmdlets and modules for the Microsoft Graph PowerShell SDK can’t imagine that such a setting exists. All we can do is look elsewhere.
The PnP PowerShell Approach
The PnP PowerShell module has been under active development for many years. Because it’s worked on by a team of people with a deep history with SharePoint (Server and Online), the interfaces are often richer and more comprehensive.
The same approach can be followed with PnP PowerShell (refer to Sean’s article for details about how to configure the app used by PnP PowerShell for a tenant). Here’s the basic code that I came up with:
Connect-PnPOnline -ClientId $PnPClientApp -Interactive -Url $SPOSite # Get all group sites that aren't archived [array]$Sites = Get-PnPTenantSite -Template "GROUP#0" -Detailed -Filter "ArchiveStatus -eq 'NotArchived'" Write-Host ("{0} group sites found" -f $Sites.Count) # Check each site for a default sensitivity label $Report = [System.Collections.Generic.List[Object]]::new() Write-Host "Checking sites for document libraries with default sensitivity labels..." ForEach ($Site in $Sites) { Connect-PnPOnline -ClientId $PnPClientApp -Url $Site.Url -Thumbprint $Thumbprint -Tenant $TenantName [array]$Lists = Get-PnPList $Lists = $Lists | Where-Object {$_.BaseType -eq 'DocumentLibrary' -and $_.Hidden -eq $false} ForEach ($List in $Lists) { If ($List.Title -in $ExcludedLibraries ) { Continue } If (!([string]::IsNullOrWhiteSpace($List.DefaultSensitivityLabelForLibrary))) { [string]$LabelGuid = $List.DefaultSensitivityLabelForLibrary $ReportLine = [PSCustomObject][Ordered]@{ SiteTitle = $Site.Title SiteUrl = $Site.Url ListTitle = $List.Title Label = $LabelsHash[$LabelGuid] LabelId = $List.DefaultSensitivityLabelForLibrary } $Report.Add($ReportLine) Write-Host ("The document library {0} in the {1} site has default sensitivity label {2} ({3})" -f $List.Title, $Site.Url, ` $LabelsHash[$List.DefaultSensitivityLabelForLibrary], $List.DefaultSensitivityLabelForLibrary) } } }
The first step is to connect to the SharePoint root for the tenant (like https://office365itpros.sharepoint.com). Next, the Get-PnPTenantSite cmdlet fetches the set of sites used by Microsoft 365 groups, excluding any archived sites (unlike Get-MgSite, the filters work properly).
The code then loops through the set of sites to find the set of lists for each site. A filter extracts the document library lists. Further processing drops lists used for the Teams Wiki, Site pages, Site assets, style library, and so on. The remaining lists are likely for document libraries, and the script proceeds to check their settings for a default sensitivity label. If a label is discovered, the code resolves the label GUID to the label display name and uses that in the report.
Resolving the GUID is an optional step that the script enables by connecting to Exchange Online to run the Get-Label cmdlet to fetch details of sensitivity labels used with files. The resulting hash table is used to resolve the GUIDs found in document libraries.
After all sites are processed, the script reports what it has found (Figure 1). If you wanted to check if all users have the right licenses, it would be easy to extract the membership for each site and check their licenses. I’ll leave that step as an exercise for the reader.

You can download the script from GitHub. Remember that this code is intended to illustrate a principal instead of being a fully worked-out solution. The normal caveats apply about testing code in a trial tenant before exposing it to a production tenant.
More than One Way to Run PowerShell in Microsoft 365
All of this proves that there’s usually more than one way to automate a Microsoft 365 process with PowerShell (well, in this case, only one method works). More correctly, I should say that there’s more than one module that could potentially be used to solve an automation process.
Being flexible is both a joy and problem. It’s great that Microsoft provides so many ways to interact with Microsoft 365 data. But having so many modules and so many cmdlets to work with creates a very long and steep learning curve. That’s bad. Some rationalization in Microsoft 365 PowerShell is probably necessary, but don’t expect it to happen anytime soon.