Public/Get-SharingLink.ps1

function Get-SharingLink {
    <#
    .SYNOPSIS
        Audit SharePoint Online sharing links for a site, a set of sites, or a
        whole tenant, with an oversharing risk rating on each.

    .DESCRIPTION
        Sharing links are the quietest oversharing vector in SharePoint - they do
        not show up as normal permissions, they hide as SharingLinks.<guid> groups,
        and nobody reads a list of those. This finds them through exactly those
        groups, so it only touches files that actually have a link rather than
        walking everything, and reports each link's scope, who it reaches, when it
        expires, and how risky it is.

        Read-only. Returns objects, so pipe to Remove-SharingLink to revoke, or to
        Export-SharingLinkReport / Export-Csv to keep a record.

    .PARAMETER SiteUrl
        A single site to audit.

    .PARAMETER SiteUrls
        Several sites to audit.

    .PARAMETER Tenant
        Audit every site in the tenant. Needs -TenantAdminUrl and SharePoint admin.

    .PARAMETER TenantAdminUrl
        e.g. https://contoso-admin.sharepoint.com. Required with -Tenant.

    .PARAMETER ClientId
        Entra ID app registration client ID. Not needed with -UseExistingConnection.

    .PARAMETER IncludeFolders
        Also report links on folders, not just files.

    .PARAMETER MinimumRisk
        Only return links at this risk level or above (Low, Medium, High, Critical).

    .EXAMPLE
        Get-SharingLink -SiteUrl "https://contoso.sharepoint.com/sites/Sales" -ClientId $appId -Interactive

    .EXAMPLE
        Connect-PnPOnline -Url "https://contoso.sharepoint.com/sites/Sales" -ClientId $appId -Interactive
        Get-SharingLink -SiteUrl "https://contoso.sharepoint.com/sites/Sales" -UseExistingConnection

    .EXAMPLE
        Get-SharingLink -Tenant -TenantAdminUrl "https://contoso-admin.sharepoint.com" `
            -ClientId $appId -CertificatePath .\audit.pfx -Tenant contoso.onmicrosoft.com `
            -MinimumRisk High
    #>

    [CmdletBinding(DefaultParameterSetName = 'Site')]
    param(
        [Parameter(Mandatory, ParameterSetName = 'Site')]
        [string]   $SiteUrl,

        [Parameter(Mandatory, ParameterSetName = 'Sites')]
        [string[]] $SiteUrls,

        [Parameter(Mandatory, ParameterSetName = 'Tenant')]
        [switch]   $TenantWide,

        [Parameter(ParameterSetName = 'Tenant')]
        [string]   $TenantAdminUrl,

        [string]       $ClientId,
        [string]       $Tenant,
        [string]       $CertificatePath,
        [securestring] $CertificatePassword,
        [string]       $Thumbprint,
        [switch]       $Interactive,
        [switch]       $ManagedIdentity,
        [switch]       $UseExistingConnection,

        [switch]   $IncludeFolders,

        [ValidateSet('Low','Medium','High','Critical')]
        [string]   $MinimumRisk = 'Low'
    )

    $rank = @{ Low = 0; Medium = 1; High = 2; Critical = 3 }
    $floor = $rank[$MinimumRisk]

    $connectSplat = @{
        ClientId = $ClientId; Tenant = $Tenant
        CertificatePath = $CertificatePath; CertificatePassword = $CertificatePassword
        Thumbprint = $Thumbprint; Interactive = $Interactive
        ManagedIdentity = $ManagedIdentity; UseExistingConnection = $UseExistingConnection
    }

    # Work out the list of sites to visit.
    $targets = switch ($PSCmdlet.ParameterSetName) {
        'Site'   { @($SiteUrl) }
        'Sites'  { $SiteUrls }
        'Tenant' {
            # -TenantWide is the switch that selects this set; referencing it keeps
            # the intent explicit (and satisfies the unused-parameter analyzer).
            $null = $TenantWide
            if (-not $TenantAdminUrl) { throw "-TenantWide needs -TenantAdminUrl." }
            if ($UseExistingConnection) {
                throw "-UseExistingConnection cannot be combined with -TenantWide: each site is connected to in turn, so real credentials (-ClientId + an auth method) are required."
            }
            Connect-IfNeeded -Url $TenantAdminUrl @connectSplat
            Write-Verbose "Enumerating tenant sites..."
            (Invoke-WithRetry -Because 'Get-PnPTenantSite' -Action { Get-PnPTenantSite }) |
                Where-Object { $_.Template -notlike 'REDIRECT*' } |
                Select-Object -ExpandProperty Url
        }
    }

    $total = @($targets).Count
    $n = 0
    $emitted = 0   # so an empty run can say so, instead of looking broken

    foreach ($site in $targets) {
        $n++
        Write-Progress -Activity 'Auditing sharing links' -Status "$n of $total : $site" `
                       -PercentComplete (($n / [Math]::Max($total,1)) * 100)
        try {
            Connect-IfNeeded -Url $site @connectSplat
            # Count as we stream, without collecting everything - keeps it usable
            # on a large tenant while still knowing whether anything was found.
            Get-SiteSharingLink -SiteUrl $site -IncludeFolders:$IncludeFolders |
                Where-Object { $rank[$_.RiskLevel] -ge $floor } |
                ForEach-Object { $emitted++; $_ }
        }
        catch {
            Write-Warning "Skipped $site : $($_.Exception.Message)"
        }
    }

    Write-Progress -Activity 'Auditing sharing links' -Completed

    if ($emitted -eq 0) {
        $where = if ($MinimumRisk -ne 'Low') { " at risk level $MinimumRisk or above" } else { '' }
        Write-Information "No sharing links found across $total site(s)$where." -InformationAction Continue
    }
    else {
        Write-Verbose "Found $emitted sharing link(s)."
    }
}