Archivos compartidos con personas externas
Busca en la auditoría qué se compartió con personas de fuera en los últimos días, quién lo hizo y con quién. Incluye las ligas para cualquiera.
Ejemplo en México
Distribuidora de calzado en León, 150 personas
Un vendedor se fue a la competencia y quieren saber qué listas de precios salieron de SharePoint en el último mes.
.\search-archivos-compartidos-externos.ps1 -Dias 30 -RutaCsv .\compartidos.csvObtienen quién compartió cada archivo, con qué persona externa y a qué hora de México, y retiran las ligas que ya no se justifican.
Cómo implementarlo
- Confirma con get-estado-auditoria que la auditoría esté activa.
- Ejecuta el script con -Dias y -RutaCsv e inicia sesión en Exchange Online.
- En Excel, filtra por TipoRecurso, Destinatario y Sitio; empieza por Cualquiera con la liga.
- Pide a cada dueño justificar o retirar lo que no deba estar compartido.
Ver el código completo de search-archivos-compartidos-externos.ps1
<#
.SYNOPSIS
Busca en la auditoría qué archivos se compartieron con personas externas en los últimos días.
.DESCRIPTION
Usa Search-UnifiedAuditLog con paginación por sesión (ReturnLargeSet, hasta 50,000 registros). Conserva las
ligas para cualquiera, las invitaciones externas y los eventos cuyo destinatario es de tipo Guest.
Solo lectura: no cambia nada en el tenant.
.PARAMETER Dias
Días hacia atrás. Por defecto 30.
.PARAMETER PriorizarCompletitud
Agrega -HighCompleteness: resultados más completos, pero más lentos. Microsoft lo marca en versión preliminar.
.PARAMETER ZonaHoraria
Zona horaria para mostrar las fechas.
.PARAMETER RutaCsv
Ruta del CSV de salida. Si se omite, solo se muestra en pantalla.
.EXAMPLE
.\search-archivos-compartidos-externos.ps1 -Dias 30 -RutaCsv .\compartidos.csv
.NOTES
Permisos: ninguno de Graph; usa ExchangeOnlineManagement.
Rol mínimo: Audit Logs o View-Only Audit Logs en Exchange Online.
Licencia: Microsoft Purview Audit (Standard): retención de 180 días; un año para SharePoint y OneDrive con usuarios E5 o complemento equivalente.
Licencia del código: MIT. Se entrega TAL CUAL, sin garantía. Pruébalo primero en un tenant de pruebas.
#>
[CmdletBinding()]
param(
[ValidateRange(1, 3650)]
[int]$Dias = 30,
[switch]$PriorizarCompletitud,
[string]$ZonaHoraria = 'Central Standard Time (Mexico)',
[string]$RutaCsv
)
$ErrorActionPreference = 'Stop'
$zona = [TimeZoneInfo]::FindSystemTimeZoneById($ZonaHoraria)
$limite = 50000
if (-not (Get-Command Connect-ExchangeOnline -ErrorAction SilentlyContinue)) {
throw 'Falta el módulo. Instálalo con: Install-Module ExchangeOnlineManagement -Scope CurrentUser'
}
function HoraLocal($valor) {
if (-not $valor) { return '' }
$utc = if ($valor -is [datetime]) {
if ($valor.Kind -eq 'Unspecified') { [datetime]::SpecifyKind($valor, 'Utc') } else { $valor.ToUniversalTime() }
} else {
[datetimeoffset]::Parse("$valor", [cultureinfo]::InvariantCulture, 'AssumeUniversal').UtcDateTime
}
[TimeZoneInfo]::ConvertTimeFromUtc($utc, $zona).ToString('yyyy-MM-dd HH:mm')
}
$fin = (Get-Date).ToUniversalTime()
$filtro = @{
StartDate = $fin.AddDays(-$Dias)
EndDate = $fin
Operations = 'AnonymousLinkCreated', 'SecureLinkCreated', 'AddedToSecureLink', 'SharingInvitationCreated', 'SharingSet'
}
if ($PriorizarCompletitud) { $filtro.HighCompleteness = $true }
if ($Dias -gt 180) { Write-Warning 'Sin licencia E5 o complemento equivalente, la auditoría solo conserva 180 días.' }
Connect-ExchangeOnline -ShowBanner:$false
$sesion = [guid]::NewGuid().ToString()
$vistos = [System.Collections.Generic.HashSet[string]]::new()
$registros = [System.Collections.Generic.List[object]]::new()
try {
# Con la auditoría apagada Search-UnifiedAuditLog no devuelve nada, y cero parecería un resultado limpio.
if (Get-Command Get-AdminAuditLogConfig -ErrorAction SilentlyContinue) {
if ((Get-AdminAuditLogConfig).UnifiedAuditLogIngestionEnabled -ne $true) { throw 'La auditoría unificada está apagada: este reporte no puede ver nada. Actívala y vuelve a correrlo.' }
} else {
Write-Warning 'Tu rol no permite comprobar si la auditoría está activa. Un resultado vacío podría no significar nada: confírmalo con get-estado-auditoria.'
}
# Se pide hasta que no llegue nada nuevo; siempre el mismo SessionCommand para no bajar el tope a 10,000.
do {
$pagina = @(Search-UnifiedAuditLog @filtro -SessionId $sesion -SessionCommand ReturnLargeSet -ResultSize 5000)
$nuevos = @($pagina | Where-Object { $vistos.Add([string]$_.Identity) })
foreach ($r in $nuevos) { $registros.Add($r) }
} while ($nuevos.Count -and $registros.Count -lt $limite)
} finally {
Disconnect-ExchangeOnline -Confirm:$false
}
if ($registros.Count -ge $limite) { Write-Warning "Se alcanzó el tope de $limite registros: pueden faltar eventos. Usa menos días." }
$filas = @(
foreach ($r in $registros) {
$a = try { $r.AuditData | ConvertFrom-Json } catch { $null }
$operacion = if ($a.Operation) { $a.Operation } else { $r.Operations }
$externo = $operacion -in 'AnonymousLinkCreated', 'SharingInvitationCreated' -or $a.TargetUserOrGroupType -eq 'Guest'
if ($externo) {
[pscustomobject]@{
Fecha = if ($a.CreationTime) { HoraLocal $a.CreationTime } else { "$($r.CreationDate)" }
CompartioPor = $r.UserIds
Operacion = $operacion
Recurso = $a.ObjectId
TipoRecurso = $a.ItemType
Destinatario = if ($operacion -eq 'AnonymousLinkCreated') { 'Cualquiera con la liga' } else { $a.TargetUserOrGroupName }
TipoDestino = $a.TargetUserOrGroupType
Sitio = $a.SiteUrl
IP = $a.ClientIP
}
}
}
) | Sort-Object Fecha -Descending
Write-Host "Registros de auditoría: $($registros.Count). Con destino externo: $(@($filas).Count)."
if (-not $registros.Count) { Write-Warning 'Sin registros de compartir en ese periodo. Confirma que sea correcto antes de reportarlo como limpio.' }
if ($RutaCsv) {
# Destinatarios y nombres de archivo vienen de fuera: Excel ejecutaría como fórmula los que empiecen así.
foreach ($f in $filas) { foreach ($p in $f.PSObject.Properties) { if ($p.Value -is [string] -and $p.Value -match '^[=+\-@\t\r]') { $p.Value = "'" + $p.Value } } }
# UTF-8 con BOM para que Excel en español muestre bien los acentos.
$filas | Export-Csv -Path $RutaCsv -NoTypeInformation -Encoding utf8BOM
Write-Host "CSV guardado en $RutaCsv"
} else {
$filas | Format-Table -AutoSize
}