Saltar al contenido
Microsoft scripts

Scripts de Purview

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.

Antes de correrloSolo muestra eventos de compartir, no quién abrió el archivo después. Una liga para cualquiera no identifica al destinatario. La búsqueda tiene un tope de 50,000 registros: si lo alcanza, avisa y debes usar menos días. El resultado puede estar incompleto sin -PriorizarCompletitud, opción en versión preliminar que no está en todas las organizaciones. También aparecen carpetas y sitios: revisa TipoRecurso. Si la auditoría está apagada, el script se detiene con error en vez de mostrar cero.

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.csv

Obtienen 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

  1. Confirma con get-estado-auditoria que la auditoría esté activa.
  2. Ejecuta el script con -Dias y -RutaCsv e inicia sesión en Exchange Online.
  3. En Excel, filtra por TipoRecurso, Destinatario y Sitio; empieza por Cualquiera con la liga.
  4. 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
}

Relacionados