Control 4.4: Guest and External User Access Controls — PowerShell Setup
Read the FSI PowerShell baseline first
Before running any command in this playbook, read the PowerShell Authoring Baseline for FSI Implementations. It is the canonical source for module version pinning, mutation safety (-WhatIf / SupportsShouldProcess), Dataverse compatibility, and SHA-256 evidence emission. Snippets below show the abbreviated patterns; the baseline is authoritative.
This playbook provides PowerShell automation guidance for Control 4.4.
Prerequisites
| Module | Purpose | Pinning guidance |
|---|---|---|
Microsoft.Online.SharePoint.PowerShell |
Tenant + site sharing configuration | Pin to a CAB-approved version; some cmdlets behave differently on PS 5.1 vs PS 7 (verify per the baseline) |
Microsoft.Graph.Users |
Replacement for retired Remove-SPOExternalUser |
Pin to the meta-module minor version used elsewhere in your tenant |
ExchangeOnlineManagement |
Search-UnifiedAuditLog for sharing-event audit |
Pin and use session-paginated queries (5,000-record limit) |
# Use the canonical pinned-install pattern from the FSI baseline.
# Replace <version> with the version approved by your CAB.
Install-Module -Name Microsoft.Online.SharePoint.PowerShell `
-RequiredVersion '<version>' -Repository PSGallery -Scope CurrentUser -AllowClobber -AcceptLicense
Install-Module -Name Microsoft.Graph.Users `
-RequiredVersion '<version>' -Repository PSGallery -Scope CurrentUser -AllowClobber -AcceptLicense
Install-Module -Name ExchangeOnlineManagement `
-RequiredVersion '<version>' -Repository PSGallery -Scope CurrentUser -AllowClobber -AcceptLicense
Connect to Services
param(
[Parameter(Mandatory)] [string]$TenantName # e.g., "contoso" -> contoso-admin.sharepoint.com
)
Connect-SPOService -Url "https://$TenantName-admin.sharepoint.com"
# Microsoft Graph connection for guest user lifecycle
Connect-MgGraph -Environment 'Global' -Scopes 'User.ReadWrite.All','Directory.Read.All'
Read-only inventory (safe to run anytime)
# Tenant-level sharing posture
Get-SPOTenant | Select-Object `
SharingCapability, `
SharingDomainRestrictionMode, `
SharingAllowedDomainList, `
SharingBlockedDomainList, `
DefaultSharingLinkType, `
DefaultLinkPermission, `
RequireAnonymousLinksExpireInDays, `
ExternalUserExpirationRequired, `
ExternalUserExpireInDays, `
PreventExternalUsersFromResharing, `
CoreDefaultShareLinkScope, `
OneDriveDefaultShareLinkScope
# Per-site posture for an inventoried subset
$sites = Import-Csv -Path '.\zone3-sites.csv' # Url,Zone columns
$sites | ForEach-Object {
Get-SPOSite -Identity $_.Url -ErrorAction SilentlyContinue |
Select-Object Url, SharingCapability, DisableSharingForNonOwnersStatus, ExternalUserExpirationInDays
} | Export-Csv -Path ".\evidence\4.4\site-posture-$(Get-Date -Format 'yyyyMMdd').csv" -NoTypeInformation
CoreDefaultShareLinkScope and OneDriveDefaultShareLinkScope
Newer SharePoint tenants expose CoreDefaultShareLinkScope and OneDriveDefaultShareLinkScope as the modern replacements for DefaultSharingLinkType. Both values are surfaced through Get-SPOTenant. Inspect both when documenting baseline state — Microsoft is gradually transitioning configuration semantics from the legacy parameter to the scoped parameters.
Tenant-level configuration (mutating — requires change control)
This is a tenant-affecting mutation. Follow the baseline's mutation-safety pattern: declare SupportsShouldProcess, snapshot state before changes, run with -WhatIf first, and emit hashed evidence.
[CmdletBinding(SupportsShouldProcess, ConfirmImpact='High')]
param(
[Parameter(Mandatory)] [string]$AdminUrl,
[string]$EvidencePath = '.\evidence\4.4'
)
$ErrorActionPreference = 'Stop'
New-Item -ItemType Directory -Force -Path $EvidencePath | Out-Null
$ts = Get-Date -Format 'yyyyMMdd-HHmmss'
Start-Transcript -Path "$EvidencePath\tenant-mutation-$ts.log" -IncludeInvocationHeader
Connect-SPOService -Url $AdminUrl
# Snapshot BEFORE mutating (rollback evidence)
$before = Get-SPOTenant
$before | ConvertTo-Json -Depth 10 | Set-Content "$EvidencePath\tenant-before-$ts.json"
if ($PSCmdlet.ShouldProcess($AdminUrl, 'Apply Control 4.4 tenant sharing baseline')) {
Set-SPOTenant `
-SharingCapability ExistingExternalUserSharingOnly `
-DefaultSharingLinkType Direct `
-DefaultLinkPermission View `
-RequireAnonymousLinksExpireInDays 30 `
-ExternalUserExpirationRequired $true `
-ExternalUserExpireInDays 30 `
-PreventExternalUsersFromResharing $true
}
# Snapshot AFTER + integrity hash
$after = Get-SPOTenant
$afterPath = "$EvidencePath\tenant-after-$ts.json"
$after | ConvertTo-Json -Depth 10 | Set-Content $afterPath
$hash = (Get-FileHash -Path $afterPath -Algorithm SHA256).Hash
"{ ""file"": ""$(Split-Path $afterPath -Leaf)"", ""sha256"": ""$hash"", ""generated_utc"": ""$ts"" }" |
Add-Content -Path "$EvidencePath\manifest.json"
Stop-Transcript
Always run with -WhatIf first
Set-SPOTenant -SharingCapability propagates within minutes and can disconnect active guest sessions on every site that previously inherited a more permissive value. Run with -WhatIf, communicate the change window, and have a rollback plan that re-applies the snapshot file.
Optional: domain allow-list
# Run AFTER the SharingCapability change has propagated (typically 5-15 minutes)
Set-SPOTenant `
-SharingDomainRestrictionMode AllowList `
-SharingAllowedDomainList 'approvedpartner.com trustedvendor.com regulator.gov'
Site-level configuration per zone
[CmdletBinding(SupportsShouldProcess, ConfirmImpact='High')]
param(
[Parameter(Mandatory)] [string]$SiteCsvPath, # Columns: Url, Zone
[string]$EvidencePath = '.\evidence\4.4'
)
$sites = Import-Csv -Path $SiteCsvPath
$ts = Get-Date -Format 'yyyyMMdd-HHmmss'
$results = @()
foreach ($row in $sites) {
try {
$before = Get-SPOSite -Identity $row.Url -ErrorAction Stop
$action = switch ($row.Zone) {
'3' { @{ SharingCapability = 'Disabled' } }
'2' { @{ SharingCapability = 'ExistingExternalUserSharingOnly'; ExternalUserExpirationInDays = 30; DisableSharingForNonOwnersStatus = $true } }
'1' { @{ SharingCapability = 'ExternalUserSharingOnly'; ExternalUserExpirationInDays = 90 } }
default { $null }
}
if (-not $action) { continue }
if ($PSCmdlet.ShouldProcess($row.Url, "Apply Zone $($row.Zone) sharing baseline")) {
Set-SPOSite -Identity $row.Url @action
$after = Get-SPOSite -Identity $row.Url
$results += [pscustomobject]@{
Url = $row.Url; Zone = $row.Zone
Before = $before.SharingCapability; After = $after.SharingCapability
Status = 'Applied'
}
}
} catch {
$results += [pscustomobject]@{
Url = $row.Url; Zone = $row.Zone
Before = $null; After = $null
Status = "Failed: $($_.Exception.Message)"
}
}
}
$out = "$EvidencePath\site-changes-$ts.csv"
$results | Export-Csv -Path $out -NoTypeInformation
"{ ""file"": ""$(Split-Path $out -Leaf)"", ""sha256"": ""$((Get-FileHash $out -Algorithm SHA256).Hash)"", ""generated_utc"": ""$ts"" }" |
Add-Content -Path "$EvidencePath\manifest.json"
Guest user inventory and lifecycle
Remove-SPOExternalUser was retired by Microsoft on July 29, 2024. Use Microsoft Graph PowerShell (Remove-MgUser) for guest removal going forward.
# Tenant-wide external user inventory (paged)
$externalUsers = @()
$page = Get-SPOExternalUser -PageSize 50 -Position 0
while ($page) {
$externalUsers += $page
if ($page.Count -lt 50) { break }
$page = Get-SPOExternalUser -PageSize 50 -Position ($externalUsers.Count)
}
$externalUsers | Select-Object DisplayName, Email, AcceptedAs, WhenCreated, InvitedBy |
Export-Csv -Path ".\evidence\4.4\external-users-$(Get-Date -Format 'yyyyMMdd').csv" -NoTypeInformation
# Remove a specific external user (replacement for Remove-SPOExternalUser)
# Requires Microsoft.Graph.Users with User.ReadWrite.All
$guest = Get-MgUser -Filter "userType eq 'Guest' and mail eq 'user@external.com'"
if ($guest) {
if ($PSCmdlet.ShouldProcess($guest.UserPrincipalName, 'Remove guest user')) {
Remove-MgUser -UserId $guest.Id
}
}
Audit-log query for sharing events
Connect-IPPSSession # Security & Compliance PowerShell
$sessionId = [guid]::NewGuid().ToString()
$startDate = (Get-Date).AddDays(-30)
$endDate = Get-Date
$events = @()
do {
$batch = Search-UnifiedAuditLog -StartDate $startDate -EndDate $endDate `
-RecordType SharePointSharingOperation `
-SessionId $sessionId -SessionCommand ReturnLargeSet -ResultSize 5000
$events += $batch
} while ($batch.Count -eq 5000)
$events | Select-Object CreationDate, UserIds, Operations, AuditData |
Export-Csv -Path ".\evidence\4.4\sharing-events-$(Get-Date -Format 'yyyyMMdd').csv" -NoTypeInformation
Pagination is mandatory in FSI tenants
A single Search-UnifiedAuditLog call returns at most 5,000 records and silently truncates beyond that. The pagination loop above is the audit-defensible pattern.
Rollback
If a tenant or site change must be reverted:
- Locate the most recent
tenant-before-*.json(orsite-changes-*.csv) underevidence\4.4\. - For tenant changes, re-apply the snapshot values via
Set-SPOTenantusing the captured property values. - For site changes, run the bulk site script again with
Zonecolumn values that map to the priorBeforeSharingCapability. - Document the rollback in the original change ticket.
Back to Control 4.4 | Portal Walkthrough | Verification Testing | Troubleshooting
Updated: May 2026 | Version: v1.6.2 | UI Verification Status: Current