Private/Resolve-SharingLinkAudience.ps1

function Test-SharingLinkAudience {
    <#
        Does THIS link reach THIS user?

        Split out from the crawl deliberately: this is pure decision logic with
        no SharePoint calls in it, so it can be unit-tested. It is also the most
        dangerous code in the module - getting it wrong either tells an admin a
        file is safe when anyone in the company can open it, or buries them in
        findings that do not apply. Both failures are silent.

        The rules:
          Anonymous - anyone at all, signed in or not. Always applies.
          Organization - anyone internal. Applies to any user we are asked about,
                          because we are only ever asked about internal users.
          Users - restricted to named people. Applies ONLY if this user is
                          one of them.

        Returns $null when the link does not reach the user, so callers can just
        filter. Scope comes from the LINK, never from the group name - a modern
        link's group is named "...Flexible..." regardless of its real scope.
    #>

    param(
        # The .Link facet of a Get-PnPFileSharingLink result.
        [Parameter(Mandatory)] $Link,
        [Parameter(Mandatory)] [string] $UserUpn,
        # The .GrantedToIdentitiesV2 collection, when the scope is Users.
        $GrantedTo
    )

    if (-not $Link) { return $null }

    $scope = "$($Link.Scope)"
    $type  = "$($Link.Type)"

    $applies = $false
    $why     = ''

    if ($scope -match '^Anonymous') {
        $applies = $true; $why = 'anyone with the link'
    }
    elseif ($scope -match '^Organization') {
        $applies = $true; $why = 'anyone in the organisation'
    }
    elseif ($scope -match '^Users') {
        foreach ($g in @($GrantedTo)) {
            if (-not $g) { continue }
            $u = if ($g.PSObject.Properties.Name -contains 'User') { $g.User } else { $null }
            if (-not $u) { continue }
            foreach ($prop in 'Email', 'UserPrincipalName') {
                if ($u.PSObject.Properties.Name -contains $prop -and
                    "$($u.$prop)" -eq $UserUpn) {
                    $applies = $true; $why = 'named recipient'
                }
            }
        }
    }

    if (-not $applies) { return $null }

    [pscustomobject]@{
        # A View link grants Read; Edit and Review both let the user change things.
        Level = $(if ($type -match 'Edit|Review') { 'Edit' } else { 'Read' })
        Route = [pscustomobject]@{
            Route      = "Sharing link - $scope / $type ($why)"
            Type       = 'Overshared'
            Permission = $type
        }
    }
}

function Resolve-SharingLinkAudience {
    <#
        Ask a file what links it carries, and return the ones that reach this
        user. The I/O half of the pair - the decision half is
        Test-SharingLinkAudience, which is where the tests live.

        Sharing links are NOT a per-user grant. Verified 23 Jul 2026: an
        organisation-scope link put a SharingLinks.* group on an item's role
        assignments, yet GetUserEffectivePermissions for an ordinary org user
        returned every flag False. So this must be asked separately from "what
        can this user do here", and its answer trusted independently.
    #>

    param(
        [Parameter(Mandatory)] [string] $FileRef,
        [Parameter(Mandatory)] [string] $UserUpn
    )

    $links = @()
    try {
        $links = @(Get-PnPFileSharingLink -Identity $FileRef -ErrorAction Stop)
    } catch {
        # Folders and plain list items are not files - nothing to resolve.
        Write-Verbose "No file sharing links on $FileRef : $($_.Exception.Message)"
        return
    }

    foreach ($l in $links) {
        if (-not $l) { continue }
        $link = if ($l.PSObject.Properties.Name -contains 'Link') { $l.Link } else { $null }
        $granted = if ($l.PSObject.Properties.Name -contains 'GrantedToIdentitiesV2') { $l.GrantedToIdentitiesV2 } else { $null }

        $verdict = Test-SharingLinkAudience -Link $link -UserUpn $UserUpn -GrantedTo $granted
        if ($verdict) { $verdict }
    }
}