Private/Resolve-SPMSharingLink.ps1
|
function Resolve-SPMSharingLink { <# .SYNOPSIS Resolves what a sharing link shares and how it was created. .DESCRIPTION The item GUID inside a SharingLinks group name is the file/folder UniqueId. One ListItemAllFields lookup yields the path, the real type (FileSystemObjectType) and the list/item ids; GetSharingInformation then adds creator, creation date and expiration. Folders shared anonymously or organization-wide are additionally checked for suspicious filenames. Shared by the deep scan (Get-SPMSiteScan) and the express scan (Get-SPMSharingOverview) so both see identical link forensics. .PARAMETER ItemId UniqueId of the shared file or folder (from the link group name). .PARAMETER Kind Link kind from the group name: AnonymousView, OrganizationEdit, Flexible, ... .PARAMETER ShareId Share GUID from the group name - identifies the individual link on the item. .PARAMETER Cache Hashtable reused across calls so one item with several links is fetched once. #> [CmdletBinding()] param( [Parameter(Mandatory)][string]$ItemId, [Parameter(Mandatory)][string]$Kind, [string]$ShareId, [hashtable]$Cache = @{} ) $out = [ordered]@{ target = $null targetType = 'file' created = $null createdBy = $null expiration = $null suspiciousFiles = @() } $li = $null try { $li = Invoke-SPMWithRetry { Invoke-PnPSPRestMethod -Url "/_api/web/GetFileById('$ItemId')/ListItemAllFields?`$select=Id,FileSystemObjectType,FileRef,ParentList/Id&`$expand=ParentList" } } catch { try { $li = Invoke-PnPSPRestMethod -Url "/_api/web/GetFolderById('$ItemId')/ListItemAllFields?`$select=Id,FileSystemObjectType,FileRef,ParentList/Id&`$expand=ParentList" } catch { Write-Verbose "Sharing link target $ItemId could not be resolved." } } if ($li -and [int]$li.FileSystemObjectType -eq 1) { $out.targetType = 'folder' } if ($li -and $li.FileRef) { $out.target = [string]$li.FileRef } # link metadata: who created it, when, and whether it ever expires try { if (-not $Cache.ContainsKey($ItemId)) { $base = if ($out.targetType -eq 'folder') { "GetFolderById('$ItemId')" } else { "GetFileById('$ItemId')" } # the empty JSON body is required: without it SharePoint's OData parser # fails with "Cannot handle the data at position 0" $Cache[$ItemId] = Invoke-SPMWithRetry { try { Invoke-PnPSPRestMethod -Method Post -Url "/_api/web/$base/ListItemAllFields/GetSharingInformation?`$Expand=permissionsInformation" -Content '{}' } catch { if (-not $li) { throw } # canonical list-item form; some tenants reject the shorthand app-only Invoke-PnPSPRestMethod -Method Post -Url "/_api/web/Lists('$($li.ParentList.Id)')/GetItemById($($li.Id))/GetSharingInformation?`$Expand=permissionsInformation" -Content '{}' } } } $si = $Cache[$ItemId] $silinks = @($si.permissionsInformation.links) $link = @($silinks | Where-Object { [string]$_.linkDetails.ShareId -eq $ShareId }) | Select-Object -First 1 if ($link) { $ld = $link.linkDetails # PnP deserializes timestamps into [datetime]; normalize to ISO 8601 $toIso = { param($v) if ($v -is [datetime]) { $v.ToUniversalTime().ToString('o') } else { [string]$v } } if ($ld.Created) { $out.created = & $toIso $ld.Created } if ($ld.CreatedBy -and $ld.CreatedBy.name) { $out.createdBy = [string]$ld.CreatedBy.name } if ($ld.Expiration) { $out.expiration = & $toIso $ld.Expiration } } elseif ($silinks.Count) { Write-Warning "Sharing link $ShareId not found in sharing information of item $ItemId ($($silinks.Count) links returned)." } } catch { Write-Warning "Sharing information for item $ItemId not available: $($_.Exception.Message)" } # suspicious filenames inside anonymously/org-wide shared folders if ($out.targetType -eq 'folder' -and $Kind -match '^(Anonymous|Organization)') { try { $files = @((Invoke-PnPSPRestMethod -Url "/_api/web/GetFolderById('$ItemId')/Files?`$select=Name&`$top=200").value) $sus = @($files | Where-Object { $_.Name -match '(?i)passwor|pwd|credential|secret|geheim|vertraulich|confidential|iban|salar|lohn|gehalt|\.pfx$|\.pem$|\.key$|\.ppk$' } | ForEach-Object { [string]$_.Name } | Select-Object -First 5) if ($sus.Count) { $out.suspiciousFiles = @($sus) } } catch { Write-Verbose "File check for shared folder $ItemId failed." } } return [pscustomobject]$out } |