SharePoint (2003 thru Online): 2026

Tuesday, September 22, 2026

SharePoint Admin Agent

Here's a consolidated view about the SharePoint Admin Agent, the roles needed, and the impact of using PIM (Privileged Identity Management).

What can the SharePoint Admin Agent do?

The SharePoint Admin Agent can help administrators:

  • Provide guidance and best practices for SharePoint administration.
  • Search SharePoint sites based on site properties.
  • Retrieve recent actions performed in the SharePoint Admin Center.
  • Analyze organization-level and geo-level settings.
  • Review storage usage, storage trends, and storage capacity information.
  • Retrieve information about:
    • Microsoft 365 Backup protected sites (protection units)
    • Restore points
    • Restore sessions
  • Answer SharePoint and Microsoft 365 administration questions.

What it cannot do:

  • Make changes directly in your tenant.
  • Execute administrative actions on your behalf.
  • Manage permissions or perform role assignments.
  • Handle non-SharePoint administrative workloads.

What roles are needed for maximum utilization?

Minimum Required

SharePoint Advanced Management Administrator

This role is the primary role for unlocking SharePoint Admin Agent governance capabilities such as:

  • Copilot readiness assessments
  • Oversharing detection
  • Access governance analysis
  • Site lifecycle insights
  • Remediation recommendations

Strongly Recommended

SharePoint Administrator

This role allows administrators to act on the recommendations generated by the agent:

  • Manage sites
  • Configure tenant sharing settings
  • Manage site admins
  • Configure storage settings
  • Perform tenant-wide SharePoint administration

Optional

Global Administrator

Provides the broadest set of Microsoft 365 administrative permissions, including:

  • Role assignment
  • Tenant-wide administration
  • SharePoint administration

Because of its broad permissions, this role is typically reserved for a limited number of administrators.

Best Role Combination

For most organizations, the ideal combination is:

✅ SharePoint Advanced Management Administrator
✅ SharePoint Administrator

This provides both:

  • Full SharePoint Admin Agent visibility
  • Ability to execute resulting administrative actions

without requiring Global Administrator privileges.

If both roles are assigned through PIM (Privileged Identity Management)

Pros

Security and Least Privilege

  • No standing administrative access.
  • Privileges are granted only when needed.
  • Reduces attack surface and credential exposure.

Compliance and Auditability

  • Every activation is logged.
  • Clear audit trail for who activated access and why.
  • Helps satisfy compliance requirements.

Just-in-Time Administration

  • Roles are automatically removed after the activation window.
  • Reduces risk of forgotten privileged accounts.

MFA and Approval Controls

  • Can require:
    • MFA during activation
    • Business justification
    • Approval workflows

This significantly improves the security posture.

Cons

Additional Friction

  • Administrators must activate roles before using the agent fully.
  • Can slow down routine administrative work.

Possible Delays During Incidents

  • If approval workflows are required, urgent investigations may be delayed.

Session Interruptions

  • If the activation window expires while working with SharePoint Admin Agent recommendations, administrators may need to reactivate their role.

Operational Overhead

  • PIM policies require ongoing governance:
    • Approval management
    • Activation duration tuning
    • Audit review

My Recommendation

For a security-conscious Microsoft 365 environment, a common best practice is:

RoleAssignment Method
SharePoint Advanced Management AdministratorPIM Eligible
SharePoint AdministratorPIM Eligible
Global AdministratorPIM Eligible only (not permanently assigned)

This delivers the best balance between:

  • Maximum SharePoint Admin Agent functionality
  • Strong security controls
  • Reduced standing privilege risk

For someone regularly working with Microsoft Security and SharePoint governance, this model is generally considered the enterprise-grade approach.

Wednesday, March 11, 2026

PowerShell Script to update Company logos across all SharePoint Online Sites

We apply Company logos across all SharePoint Online Site Collections and subsites. When a new logo is designed, I receive requests to update them. The PowerShell script below simplifies this process.

While running this script, you will get a window prompt to login with your Global Admin/SharePoint Admin account. Enter credentials and proceed forward.

# --------------------------
# Configuration
# --------------------------
$clientId = "12a34567-f123-4567-890e-1cch23456789"
$SiteCollectionUrl = "https://yourtenant.sharepoint.com/sites/company"
$LogoUrl = "https://yourtenant.sharepoint.com/sites/BrandGuide/Images/Company_Logo.png"

# Connect to root once (to enumerate subsites)
Connect-PnPOnline -Url $SiteCollectionUrl -Interactive -ClientId $clientId

# Get root + all subsites (recursive)
$webs = Get-PnPSubWeb -Recurse -IncludeRootWeb

# ---- Count of all sites (webs) ----
$totalSites = $webs.Count
Write-Host "Total sites (root + all subsites) found: $totalSites" -ForegroundColor Cyan

# Detect if your Connect-PnPOnline supports -ReturnConnection
$hasReturnConnection = (Get-Command Connect-PnPOnline).Parameters.ContainsKey("ReturnConnection")

$success = 0
$failed  = 0

foreach ($w in $webs) {
    try {
        Write-Host "[$success/$totalSites] Updating logo for: $($w.Title) -> $($w.Url)" -ForegroundColor White

        if ($hasReturnConnection) {
            $conn = Connect-PnPOnline -Url $w.Url -Interactive -ClientId $clientId -ReturnConnection
            Set-PnPWeb -Connection $conn -SiteLogoUrl $LogoUrl   # Set-PnPWeb works on current web / via -Connection
        }
        else {
            Connect-PnPOnline -Url $w.Url -Interactive -ClientId $clientId
            Set-PnPWeb -SiteLogoUrl $LogoUrl                    #
        }

        $success++
        Write-Host "SUCCESS: $($w.Url)" -ForegroundColor Green
    }
    catch {
        $failed++
        Write-Host "FAILED: $($w.Url) | $($_.Exception.Message)" -ForegroundColor Red
    }
}

Write-Host "Logo update process complete." -ForegroundColor Cyan
Write-Host "Summary: Total=$totalSites | Success=$success | Failed=$failed" -ForegroundColor Yellow

The result shows as below.

Monday, February 23, 2026

M365 Tenant - SharePoint Online Storage Cleanup

Items in the SharePoint Online Recycle Bin, including both the first and second stages, contribute to the site's storage quota for 93 days. Storage space is only released once items are permanently deleted. 

If there is a retention policy, deleted items may be moved to the Preservation Hold Library, which also consumes storage. 

To free up space right away, delete items from the second-stage Recycle Bin, and then clean up the Preservation Hold Library.

The PowerShell script below is the most effective way to carry out these tasks across all site collections in the tenant.

#____________________________________________________________________________________________________________________________
#==================================================================================================
# OSI - Retention Exception Update + PHL Cleanup + Recycle Bin Cleanup (PnP)
# Single-Shot Mode (≤100 sites) — OPTIMIZED with PnP Batching
#==================================================================================================

#------------------------------ CONFIG -------------------------------------
$PolicyName               = "Document Retention Policy"
$ExceptionsCsvPath        = "E:\Reports\RP_Sites_2.csv"
$SitesCsvPath             = "E:\Reports\Sites_2.csv"

$IPPSSessionUPN           = "admin@spadmins.onmicrosoft.com"
$PnPClientId              = "12a34567-f123-4567-891e-2aaf34567890"

$PHLPageSize              = 2000
$BatchChunkSize           = 100        # ← items per PnP batch (100 is the sweet spot)

$EnableRecycleBinCleanup  = $true
$RecycleBinRowLimit       = 0

#------------------------------ SECTION 1: RETENTION POLICY EXCEPTIONS ---------------------------
Write-Host "=== Updating Retention Policy Exceptions (Single-Shot) ===" -ForegroundColor Cyan

[array]$excludeSites = Import-Csv -Path $ExceptionsCsvPath |
    Select-Object -ExpandProperty URL |
    ForEach-Object { $_.Trim().TrimEnd("/") } |
    Where-Object { $_ } |
    Sort-Object -Unique

Write-Host "Loaded $($excludeSites.Count) exception site URLs from $ExceptionsCsvPath"

if ($excludeSites.Count -gt 100) {
    Write-Host "ERROR: $($excludeSites.Count) sites exceed the 100-site static scoping limit." -ForegroundColor Red
    Write-Host "Use batched mode with deployment wait, or switch to Adaptive Scopes." -ForegroundColor Red
    return
}

Connect-IPPSSession -UserPrincipalName $IPPSSessionUPN

try {
    Set-RetentionCompliancePolicy `
        -Identity $PolicyName `
        -AddSharePointLocationException $excludeSites

    Write-Host "Successfully added all $($excludeSites.Count) exception sites in one call." -ForegroundColor Green
}
catch {
    Write-Host "FAILED to add exceptions: $($_.Exception.Message)" -ForegroundColor Red
}

Write-Host "Retention exception update completed." -ForegroundColor Cyan

#------------------------------ SECTION 2: PHL CLEANUP + RECYCLE BIN CLEANUP ---------------------

Write-Host "`n=== PHL Cleanup + Recycle Bin Cleanup ===" -ForegroundColor Cyan

$sites = Import-Csv -Path $SitesCsvPath
Write-Host "Loaded $($sites.Count) sites from $SitesCsvPath"

$hasClearRecycleCmd = $null -ne (Get-Command Clear-PnPRecycleBinItem -ErrorAction SilentlyContinue)

foreach ($site in $sites) {
    $siteUrl = $site.URL.Trim().TrimEnd("/")
    if (-not $siteUrl) { continue }

    Write-Host "`nProcessing site: $siteUrl" -ForegroundColor Yellow

    try {
        Connect-PnPOnline -Url $siteUrl -Interactive -ClientId $PnPClientId -ErrorAction Stop
    }
    catch {
        Write-Host "Failed to connect to $siteUrl : $($_.Exception.Message)" -ForegroundColor Red
        continue
    }

    # ── PHL Cleanup ──────────────────────────────────────────────────────
    $phl = Get-PnPList -Identity "Preservation Hold Library" -ErrorAction SilentlyContinue
    if (-not $phl) {
        Write-Host "No Preservation Hold Library found at $siteUrl" -ForegroundColor DarkGray
    }
    else {
        Write-Host "Preservation Hold Library found at $siteUrl" -ForegroundColor Green

        $items = @()
        try {
            $items = Get-PnPListItem -List "Preservation Hold Library" -PageSize $PHLPageSize -ScriptBlock {
                param($pagedItems)
                $pagedItems.Context.ExecuteQuery()
            }
        }
        catch {
            Write-Host "Failed to list PHL items at $siteUrl : $($_.Exception.Message)" -ForegroundColor Red
            $items = @()
        }

        if ($items.Count -gt 0) {
            Write-Host "$($items.Count) items found in Preservation Hold Library — deleting in batches of $BatchChunkSize..." -ForegroundColor Cyan

            # ╔══════════════════════════════════════════════════════════════╗
            # ║  OPTIMIZATION: PnP Batching — replaces item-by-item delete  ║
            # ║  Groups up to $BatchChunkSize requests into ONE API call    ║
            # ╚══════════════════════════════════════════════════════════════╝
            $totalDeleted = 0
            for ($i = 0; $i -lt $items.Count; $i += $BatchChunkSize) {

                $batch = New-PnPBatch

                $end = [Math]::Min($i + $BatchChunkSize - 1, $items.Count - 1)
                foreach ($item in $items[$i..$end]) {
                    Remove-PnPListItem -List "Preservation Hold Library" -Identity $item.Id -Batch $batch
                }

                try {
                    Invoke-PnPBatch -Batch $batch -ErrorAction Stop
                    $totalDeleted += ($end - $i + 1)
                    Write-Host "  Batch deleted items $($i+1)–$($end+1) of $($items.Count)" -ForegroundColor DarkCyan
                }
                catch {
                    Write-Host "  Batch failed at items $($i+1)–$($end+1): $($_.Exception.Message)" -ForegroundColor DarkYellow
                }
            }
            Write-Host "PHL cleanup complete: $totalDeleted / $($items.Count) items deleted" -ForegroundColor Green
        }
        else {
            Write-Host "No items found in Preservation Hold Library" -ForegroundColor DarkGray
        }
    }

    # ── Recycle Bin Cleanup ──────────────────────────────────────────────
    if ($EnableRecycleBinCleanup) {
        if (-not $hasClearRecycleCmd) {
            Write-Host "Clear-PnPRecycleBinItem cmdlet not found. Per PnP docs, it may require PnP.PowerShell Nightly. Skipping recycle bin cleanup." -ForegroundColor DarkYellow
        }
        else {
            try {
                if ($RecycleBinRowLimit -gt 0) {
                    Clear-PnPRecycleBinItem -All -Force -RowLimit $RecycleBinRowLimit
                }
                else {
                    Clear-PnPRecycleBinItem -All -Force
                }
                Write-Host "Recycle bins cleared for: $siteUrl" -ForegroundColor Green
            }
            catch {
                Write-Host "Failed to clear recycle bins for $siteUrl : $($_.Exception.Message)" -ForegroundColor Red
            }
        }
    }
}

Write-Host "`n=== DONE ===" -ForegroundColor Cyan

'Filter by' option for the Name column in SPO Document Library.

When I received this requirement, I reviewed several blogs and Google AI instructions, but none addressed my goal. After further research, I was able to achieve it and would like to share an article that aims to enable the 'Filter by' option for the Name column.

When a document library is first created in Modern SharePoint Online, you'll see the Name, Modified, and Modified by columns, with All Documents set as the default view.

The 'Filter by' option is available for the Modified and Modified by columns, but not for the Name column.



Top right down to '+ Create or upload' you will see Options (click on it) > Edit View.

In Edit View, under columns, after scrolling down you will notice three types of Name columns as shown below.

1. In Display, Uncheck, Name (linked to document with edit menu).
2. In Display, Check, Name (for use in forms) replace the Position from left.
3. Click OK to save the modifications.


Once redirected to the view, you will notice Filter by option for the Name column.