Skip to content

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, sovereign-cloud (GCC / GCC High / DoD) endpoints, 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

Sovereign-aware connection (Commercial / GCC / GCC High / DoD)

Per the baseline, sovereign-cloud tenants must pass explicit endpoint parameters or risk false-clean evidence from queries hitting the wrong cloud.

param(
    [Parameter(Mandatory)] [string]$TenantName,                 # e.g., "contoso" -> contoso-admin.sharepoint.com
    [ValidateSet('Commercial','GCC','GCCHigh','DoD')] [string]$Cloud = 'Commercial'
)

# SharePoint admin URL pattern by cloud (verify on Microsoft Learn for current values)
$adminHost = switch ($Cloud) {
    'Commercial' { "$TenantName-admin.sharepoint.com" }
    'GCC'        { "$TenantName-admin.sharepoint.com" }
    'GCCHigh'    { "$TenantName-admin.sharepoint.us" }
    'DoD'        { "$TenantName-admin.dps.mil" }
}
Connect-SPOService -Url "https://$adminHost"

# Microsoft Graph connection for guest user lifecycle
$graphEnv = @{ Commercial='Global'; GCC='USGov'; GCCHigh='USGovDoD'; DoD='USGovDoD' }[$Cloud]
Connect-MgGraph -Environment $graphEnv -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:

  1. Locate the most recent tenant-before-*.json (or site-changes-*.csv) under evidence\4.4\.
  2. For tenant changes, re-apply the snapshot values via Set-SPOTenant using the captured property values.
  3. For site changes, run the bulk site script again with Zone column values that map to the prior Before SharingCapability.
  4. Document the rollback in the original change ticket.

Back to Control 4.4 | Portal Walkthrough | Verification Testing | Troubleshooting


Updated: April 2026 | Version: v1.4.0 | UI Verification Status: Current