Private/M365Monitor/Detections/Test-M365ExternalSharingChange.ps1

# PSGuerrilla - Jim Tyler, Microsoft MVP - CC BY 4.0
# https://github.com/jimrtyler/PSGuerrilla | https://creativecommons.org/licenses/by/4.0/
# AI/LLM use: see AI-USAGE.md for required attribution
function Test-M365ExternalSharingChange {
    [CmdletBinding()]
    param(
        [PSCustomObject[]]$Events = @()
    )

    $results = [System.Collections.Generic.List[PSCustomObject]]::new()

    # Sharing capability levels (ordered from most restrictive to least)
    $sharingLevels = @{
        'Disabled'                        = 0
        'ExistingExternalUserSharingOnly' = 1
        'ExternalUserSharingOnly'         = 2
        'ExternalUserAndGuestSharing'     = 3
        'Anyone'                          = 4
    }

    foreach ($event in $Events) {
        $activity = $event.Activity ?? ''
        $targetName = $event.TargetName ?? ''
        $sharingWeakened = $false
        $changeDetails = [System.Collections.Generic.List[string]]::new()
        $oldLevel = -1
        $newLevel = -1

        foreach ($prop in $event.ModifiedProps) {
            $propName = $prop.Name ?? ''
            $newVal = $prop.NewValue ?? ''
            $oldVal = $prop.OldValue ?? ''

            # SharingCapability changes
            if ($propName -match 'SharingCapability|SharingAllowed|ExternalSharing') {
                $cleanOld = ($oldVal -replace '"', '').Trim()
                $cleanNew = ($newVal -replace '"', '').Trim()

                # Determine if sharing was weakened
                foreach ($level in $sharingLevels.Keys) {
                    if ($cleanOld -match $level) { $oldLevel = $sharingLevels[$level] }
                    if ($cleanNew -match $level) { $newLevel = $sharingLevels[$level] }
                }

                if ($newLevel -gt $oldLevel -and $oldLevel -ge 0) {
                    $sharingWeakened = $true
                    $changeDetails.Add("$propName weakened from '$cleanOld' to '$cleanNew'")
                } elseif ($cleanNew -match 'true|enabled|Anyone' -and $cleanOld -match 'false|disabled') {
                    $sharingWeakened = $true
                    $changeDetails.Add("$propName enabled: '$cleanOld' -> '$cleanNew'")
                } else {
                    $changeDetails.Add("$propName changed: '$cleanOld' -> '$cleanNew'")
                }
            }

            # Anonymous link settings
            if ($propName -match 'AnonymousLink|DefaultLink|RequireAnonymousLink') {
                $cleanNew = ($newVal -replace '"', '').Trim()
                $cleanOld = ($oldVal -replace '"', '').Trim()

                if ($cleanNew -match 'View|Edit|AnonymousAccess|true' -and $cleanOld -notmatch 'View|Edit|AnonymousAccess|true') {
                    $sharingWeakened = $true
                    $changeDetails.Add("Anonymous link access enabled: $propName = '$cleanNew'")
                } else {
                    $changeDetails.Add("$propName changed: '$cleanOld' -> '$cleanNew'")
                }
            }

            # Link expiration removed or extended
            if ($propName -match 'ExpirationDays|LinkExpiration|DefaultLinkExpiration') {
                $cleanNew = ($newVal -replace '"', '').Trim()
                $cleanOld = ($oldVal -replace '"', '').Trim()

                if ($cleanNew -eq '0' -or $cleanNew -eq '' -or $cleanNew -eq 'null') {
                    $sharingWeakened = $true
                    $changeDetails.Add("Link expiration removed: $propName was '$cleanOld'")
                } elseif ($cleanNew -and $cleanOld) {
                    try {
                        if ([int]$cleanNew -gt [int]$cleanOld) {
                            $sharingWeakened = $true
                            $changeDetails.Add("Link expiration extended: $propName from $cleanOld to $cleanNew days")
                        }
                    } catch { }
                }
            }

            # Guest access settings
            if ($propName -match 'AllowGuestAccess|GuestAccess|ShowPeoplePickerSuggestionsForGuestUsers') {
                $cleanNew = ($newVal -replace '"', '').Trim()
                if ($cleanNew -match 'true|enabled') {
                    $changeDetails.Add("Guest access setting enabled: $propName")
                }
            }
        }

        # Severity assessment
        $severity = if ($sharingWeakened -and $newLevel -ge 3) { 'High' }
                    elseif ($sharingWeakened) { 'Medium' }
                    elseif ($activity -match 'anonymous|anyone') { 'Medium' }
                    else { 'Low' }

        $description = if ($sharingWeakened) {
            "SharePoint sharing policy weakened on '$targetName' by $($event.Actor)"
        } else {
            "SharePoint sharing policy modified on '$targetName' by $($event.Actor)"
        }

        $results.Add([PSCustomObject]@{
            Timestamp     = $event.Timestamp
            Actor         = $event.Actor
            DetectionType = 'm365ExternalSharingChange'
            Description   = $description
            Details       = @{
                TargetSite      = $targetName
                Activity        = $activity
                SharingWeakened = $sharingWeakened
                OldSharingLevel = $oldLevel
                NewSharingLevel = $newLevel
                ChangeNotes     = @($changeDetails)
                ModifiedProps   = $event.ModifiedProps
            }
            Severity      = $severity
        })
    }

    return @($results)
}