-
Notifications
You must be signed in to change notification settings - Fork 0
SharePointAudit
A PowerShell supplement to GraphRunner that provides a three-phase workflow for enumerating SharePoint site collections, testing per-site Graph API access, and searching accessible sites for sensitive files.
GraphRunner's built-in SharePoint functions hit two problems in restricted environments:
-
Get-SharePointSiteURLsfails silently. The function uses the Graph Search API to enumerate drives. In tenants where that endpoint is restricted to privileged accounts, it returns no results and the caller has no site list to work from. -
Invoke-SearchSharePointAndOneDrivedepends on enumeration succeeding. When site discovery fails, the search has no scope and either returns nothing or errors out.
Resume-SharePointAudit.ps1 splits the work into three independent phases so each can be run, retried, or skipped without affecting the others, and so site discovery never blocks the search step.
Phase 1 uses the SharePoint Online PowerShell SDK (admin-only) to enumerate all site collections authoritatively. Phase 2 tests, as a standard user, which of those sites the current Graph token can actually reach. Phase 3 searches only those confirmed-accessible sites, passing them directly as contentSources to the Graph Search API rather than relying on any automatic discovery.
- Windows PowerShell 5.1 or PowerShell 7+
-
GraphRunner dot-sourced in the same session (provides
Invoke-RefreshGraphTokens,Invoke-ForgeUserAgent,Invoke-DriveFileDownload) - A valid Graph access token obtained via GraphRunner's
Get-GraphTokensorInvoke-RefreshGraphTokens -
Phase 1 only: SharePoint administrator access and the
Microsoft.Online.SharePoint.PowerShellmodule (installed automatically if absent)
# Dot-source GraphRunner first, then this file
. .\GraphRunner\GraphRunner.ps1
. .\Resume-SharePointAudit.ps1Encoding note: The file is pure ASCII. If you edit it, save as ASCII or UTF-8 with BOM. Windows PowerShell 5.1 reads files without a BOM as Windows-1252 by default, which silently corrupts multi-byte Unicode characters (em-dashes, smart quotes, etc.) and causes parse errors.
Phase 1 - Get-SPOSiteInventory (admin)
Enumerate all site collections -> SharePointSiteInventory.csv
|
v
Phase 2 - Test-SharePointSiteAccess (standard user)
Test Graph API access per site -> AccessibleSharePointSites.csv
-> InaccessibleSharePointSites.csv
|
v
Phase 3 - Invoke-SearchSharePointByList (standard user)
Search accessible sites via contentSources
-> hits appended to OutFile CSV
Phase 1 and Phase 2 are independent. If Phase 1 is not available (no admin access), supply SharePointSiteInventory.csv from another source — the only columns Phase 2 requires are Url, Title, Template, Owner, Status, StorageUsageCurrent, and LastContentModifiedDate.
Authenticate using GraphRunner before running Phase 2 or Phase 3. The ClientID used matters — the refresh token is bound to the client it was issued for, and refreshing with a different ClientID returns HTTP 400.
# Example using the Azure Portal client
Invoke-RefreshGraphTokens -TenantID "contoso.com" `
-ClientID "04b07795-8ddb-461a-bbee-02f9e1bf7b46" `
-RefreshToken "0.A..."Both Test-SharePointSiteAccess and Invoke-SearchSharePointByList auto-detect the ClientID from the appid claim in the access token JWT, so you do not need to pass -ClientID explicitly unless you want to override it.
Phase 1 (Get-SPOSiteInventory) uses an interactive browser sign-in via Connect-SPOService and does not use the $tokens object.
Enumerates all SharePoint site collections using the SharePoint Online PowerShell SDK and writes them to a CSV. Requires SharePoint administrator access. Installs Microsoft.Online.SharePoint.PowerShell automatically if it is not already present.
| Parameter | Required | Default | Description |
|---|---|---|---|
-AdminUrl |
Yes | SharePoint admin center URL, e.g. https://contoso-admin.sharepoint.com
|
|
-OutputFile |
No | SharePointSiteInventory.csv |
Path to write the inventory CSV |
| Column | Description |
|---|---|
Title |
Site display name |
Url |
Full site collection URL |
SiteId |
SPO site ID (GUID) |
Template |
Site template code (e.g. GROUP#0, SPSPERS#10, TEAMCHANNEL#1) |
Owner |
Primary site owner |
Status |
Site status (Active, etc.) |
StorageUsageCurrent |
Storage used in MB |
LastContentModifiedDate |
Last content change timestamp |
SiteCategory |
Derived from Template: Teams channel site, Microsoft 365 group / Teams parent site, OneDrive, or Other SharePoint site
|
Get-SPOSiteInventory -AdminUrl 'https://contoso-admin.sharepoint.com'
# Custom output path
Get-SPOSiteInventory -AdminUrl 'https://contoso-admin.sharepoint.com' `
-OutputFile '.\audit\sites.csv'Tests Graph API access for each site in SharePointSiteInventory.csv by issuing a GET /sites/{id} request per URL. Produces separate CSVs for accessible and inaccessible sites.
Unlike running the search blind, this phase tells you exactly which sites your token can reach before you spend time searching them. The SiteId written to AccessibleSharePointSites.csv is the Graph compound ID (hostname,guid,guid) that Phase 3 uses directly as a contentSources value.
Token handling mirrors Resume-GroupAudit.ps1: the ClientID is auto-detected from the JWT appid claim, proactive refreshes run on a configurable interval, 401 responses trigger an immediate refresh-and-retry, and 429 responses respect the Retry-After header without consuming the retry budget.
| Parameter | Required | Default | Description |
|---|---|---|---|
-Tokens |
Yes | Token object from GraphRunner | |
-InputCsv |
No | SharePointSiteInventory.csv |
Inventory CSV from Phase 1 |
-AccessibleOutputCsv |
No | AccessibleSharePointSites.csv |
Output path for accessible sites |
-InaccessibleOutputCsv |
No | InaccessibleSharePointSites.csv |
Output path for inaccessible sites |
-RefreshInterval |
No | 300 |
Seconds between proactive token refreshes |
-ClientID |
No | Auto-detected | Override the client ID used for token refresh |
| File | Description |
|---|---|
AccessibleSharePointSites.csv |
Sites the current token can reach. Contains Graph-format SiteId used by Phase 3. |
InaccessibleSharePointSites.csv |
Sites that returned a non-200 response. Result column records the reason: Access denied, Not found or inaccessible, Authentication failed, etc. |
Both files share the same columns:
| Column | Description |
|---|---|
Accessible |
True or False
|
Result |
Accessible, Access denied, Not found or inaccessible, etc. |
InputTitle |
Title from the inventory CSV |
InputUrl |
URL from the inventory CSV |
SiteId |
Graph compound site ID (hostname,guid,guid). Empty for inaccessible sites. |
DisplayName |
Graph display name. Empty for inaccessible sites. |
WebUrl |
Canonical URL from Graph. Empty for inaccessible sites. |
CreatedDateTime |
Site creation timestamp from Graph. Empty for inaccessible sites. |
Template |
Site template code (from inventory) |
SiteCategory |
Derived from Template |
Owner |
From inventory |
Status |
From inventory |
StorageUsageCurrent |
From inventory |
LastContentModifiedDate |
From inventory |
HttpStatus |
HTTP response code (200 for accessible, 401/403/404/etc. for inaccessible) |
Error |
Error message for inaccessible sites |
# Test all sites from the default inventory
Test-SharePointSiteAccess -Tokens $tokens
# Custom paths, longer refresh interval for large tenants
Test-SharePointSiteAccess -Tokens $tokens `
-InputCsv '.\audit\sites.csv' `
-AccessibleOutputCsv '.\audit\accessible.csv' `
-InaccessibleOutputCsv '.\audit\inaccessible.csv' `
-RefreshInterval 600Searches accessible SharePoint and OneDrive sites for files matching a KQL query. Rather than discovering sites at search time, it reads their Graph site IDs from AccessibleSharePointSites.csv and passes them as contentSources to the Graph Search API in batches of 15, which is below the API's 20-source limit.
Results are aggregated and deduplicated across all batches. Output format — CSV columns and interactive download behavior — is identical to GraphRunner's Invoke-SearchSharePointAndOneDrive, making the function a drop-in replacement in the detector-loop pattern from the GraphRunner wiki.
| Parameter | Required | Default | Description |
|---|---|---|---|
-Tokens |
Yes | Token object from GraphRunner | |
-SearchTerm |
Yes | KQL query string. Supports filetype:, content:, site operators, and boolean logic. |
|
-InputCsv |
No | AccessibleSharePointSites.csv |
Accessible sites CSV from Phase 2 |
-ResultCount |
No | 25 |
Results per page per batch |
-DetectorName |
No | Custom |
Label written to the Detector Name column in the output CSV |
-OutFile |
No | CSV file to append results to | |
-ReportOnly |
No | switch | Suppress the interactive download prompt |
-PageResults |
No | switch | Page through all available results rather than stopping after the first page |
-GraphRun |
No | switch | Suppress status output unless hits are found; use in detector loops |
-RefreshInterval |
No | 300 |
Seconds between proactive token refreshes |
-ClientID |
No | Auto-detected | Override the client ID used for token refresh |
Identical to GraphRunner's Invoke-SearchSharePointAndOneDrive:
| Column | Description |
|---|---|
Detector Name |
Value of -DetectorName
|
File Name |
Filename |
Size |
Human-readable file size |
Location |
Web URL of the file |
DriveItemID |
driveId:itemId string for use with Invoke-DriveFileDownload
|
Preview |
Search result snippet |
Last Modified Date |
Last modification timestamp |
# Interactive search — displays results, prompts to download
Invoke-SearchSharePointByList -Tokens $tokens -SearchTerm 'password filetype:xlsx'
# Report-only, write results to a file
Invoke-SearchSharePointByList -Tokens $tokens `
-SearchTerm 'password AND filetype:xlsx' `
-PageResults -ResultCount 500 `
-ReportOnly -OutFile '.\hits.csv'
# Scope to a subset — search OneDrive only (filter CSV first)
Import-Csv .\AccessibleSharePointSites.csv |
Where-Object { $_.SiteCategory -eq 'OneDrive' } |
Export-Csv .\onedrive_sites.csv -NoTypeInformation
Invoke-SearchSharePointByList -Tokens $tokens `
-InputCsv '.\onedrive_sites.csv' `
-SearchTerm 'confidential' `
-ReportOnly -OutFile '.\onedrive_hits.csv'# Load dependencies
. .\GraphRunner\GraphRunner.ps1
. .\Resume-SharePointAudit.ps1
# Authenticate
Invoke-RefreshGraphTokens -TenantID "contoso.com" `
-ClientID "04b07795-8ddb-461a-bbee-02f9e1bf7b46" `
-RefreshToken "0.A..."
# Phase 1: Enumerate all site collections (admin)
Get-SPOSiteInventory -AdminUrl 'https://contoso-admin.sharepoint.com'
# Phase 2: Test which sites are accessible with the current user token
Test-SharePointSiteAccess -Tokens $tokens
# Phase 3: Search accessible sites
Invoke-SearchSharePointByList -Tokens $tokens -SearchTerm 'password filetype:xlsx'The detector loop pattern from the GraphRunner wiki works unchanged with Invoke-SearchSharePointByList substituted in:
$folderName = "SharePointSearch-" + (Get-Date -Format 'yyyyMMddHHmmss')
New-Item -Path $folderName -ItemType Directory | Out-Null
$spout = "$folderName\interesting-files.csv"
$detectors = (Get-Content '.\default_detectors.json' | ConvertFrom-Json).Detectors
foreach ($detect in $detectors) {
Invoke-SearchSharePointByList -Tokens $tokens `
-SearchTerm $detect.SearchQuery `
-DetectorName $detect.DetectorName `
-PageResults -ResultCount 500 `
-ReportOnly -OutFile $spout -GraphRun
}Results from all detectors are appended to a single CSV. The -GraphRun switch suppresses per-detector output unless hits are found, keeping the terminal clean during long runs.
Use GraphRunner's Invoke-DriveFileDownload with the DriveItemID from the search output or Phase 3's interactive download prompt:
Invoke-DriveFileDownload -Tokens $tokens `
-FileName "Passwords.docx" `
-DriveItemIDs "b!wDDN4...:01AVEVEP..."This file is a supplement, not a replacement. It depends on GraphRunner being loaded and uses Invoke-RefreshGraphTokens, Invoke-ForgeUserAgent, and Invoke-DriveFileDownload from it. The search logic in Invoke-SearchSharePointByList is functionally identical to Invoke-SearchSharePointAndOneDrive — same Graph Search API, same output columns, same KQL query support — the difference is that site discovery is replaced by an explicit contentSources list derived from Phase 2.
GraphRunner is authored by Beau Bullock (@dafthack) and licensed under MIT.