> For the complete documentation index, see [llms.txt](https://docs.stoik.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.stoik.io/help/help-center-en-es/email-security/microsoft-365/purview-script.md).

# script de Purview

La versión automatizada de [Errores de activación de Purview](/help/help-center-en-es/email-security/microsoft-365/purview-errors.md). Se conecta a Exchange Online, fuerza una deshabilitación y luego una reactivación de la ingesta del registro de auditoría, y confirma el resultado. Si el tenant está deshidratado, ejecuta `Enable-OrganizationCustomization`, espera a que termine y luego vuelve a habilitar el registro de auditoría. Si la activación se rechaza de verdad, muestra un diagnóstico (roles de la cuenta, hidratación del tenant, estado de licencia y suscripción).

## Requisitos previos

* **Windows PowerShell** o **Azure Cloud Shell** (modo PowerShell). El script se niega a ejecutarse en macOS/Linux, donde el módulo de Exchange Online falla en operaciones de escritura.
* Una cuenta de administrador del tenant.

{% hint style="info" %}
¿No tienes una máquina Windows? Si el tenant tiene una suscripción activa de Azure, ejecútalo desde [Azure Cloud Shell](https://shell.azure.com) en modo PowerShell: carga el script y ejecútalo sin `-AdminUpn`. Cloud Shell reutiliza tu sesión, así que no aparece un aviso de inicio de sesión. Purview en sí no requiere una suscripción de Azure.
{% endhint %}

## Ejecutar el script

{% stepper %}
{% step %}

### Copia el script

Copie la referencia [script](#script) con el botón de copiar en la parte superior derecha del bloque de código, y guárdalo como `Repair-PurviewAuditLogging.ps1`.
{% endstep %}

{% step %}

### Permitir la ejecución de scripts

En PowerShell, ejecuta esto una vez por ventana (solo para la sesión, nada permanente):

```powershell
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass
```

{% endstep %}

{% step %}

### Ejecutarlo con la cuenta de administrador

Reemplace `<admin@clientdomain.com>` con un administrador del tenant. Se abrirá una ventana de inicio de sesión en el navegador; el script hace el resto.

```powershell
cd C:\ruta\a\la\carpeta
.\Repair-PurviewAuditLogging.ps1 -AdminUpn <admin@clientdomain.com>
```

{% endstep %}

{% step %}

### Verificar más tarde

`Falso` justo después de ejecutar es **normal**: Microsoft lo aplica en unos minutos (hasta aproximadamente 60 min). Vuelve a conectarte para comprobarlo:

```powershell
Connect-ExchangeOnline -UserPrincipalName <admin@clientdomain.com> -ShowBanner:$false
Get-AdminAuditLogConfig | Format-List UnifiedAuditLogIngestionEnabled
```

`Verdadero` significa que ya está hecho.
{% endstep %}
{% endstepper %}

## Solución de problemas

El script ejecuta un diagnóstico y te indica en cuál de los casos estás. Envía el `.log` archivo que creó a <protect@stoik.io>.

* **Tenant deshidratado**: gestionado automáticamente. El script ejecuta `Enable-OrganizationCustomization`, espera la hidratación y luego vuelve a habilitar el registro de auditoría.
* **403 Prohibido / rechazado**: un rechazo real. La cuenta no tiene el rol adecuado (el **Registros de auditoría** rol, concedido a través de Organization Management o Compliance Management), o el tenant no tiene una licencia que incluya Exchange Online.
* **Error del lado del servidor**: un "intenta de nuevo más tarde" transitorio de Microsoft. El script ya reintenta; si persiste, ejecútalo otra vez unos minutos después.

## Script

{% code title="Repair-PurviewAuditLogging.ps1" lineNumbers="true" %}

```powershell
<#
.SINOPSIS
    Repara el registro unificado de auditoría de Microsoft Purview forzando una deshabilitación y luego una reactivación.

.DESCRIPCIÓN
    REQUISITO PREVIO: ejecuta esto en Windows PowerShell (5.1 o 7+) O en Azure Cloud Shell.
    El script se niega a ejecutarse en cualquier otro lugar (macOS / Linux de escritorio) - esos entornos
    presentan peculiaridades del módulo de Exchange Online y no son compatibles.

      - Windows      -> pasa -AdminUpn (o omítelo para un inicio de sesión interactivo).
      - Cloud Shell  -> el inicio de sesión es automático (el módulo EXO reutiliza tu identidad de Cloud Shell).
                        Usa -Device solo si quieres específicamente el inicio de sesión con código de dispositivo.

    Incluso cuando el registro de auditoría ya aparece como habilitado, el portal de Purview puede quedarse atascado en
    su error. Forzar una deshabilitación y luego una reactivación es lo que lo desbloquea.

    La reactivación es asíncrona: Microsoft puede tardar hasta ~60 minutos en reflejarla, por lo que una
    lectura nueva aún podría mostrar Falso justo después de una llamada correcta. Eso es normal.

    Si la activación se rechaza de verdad, el script ejecuta un diagnóstico: cuenta conectada y
    roles, estado de hidratación del tenant, antigüedad del tenant y estado de suscripción/licencia (los dos últimos
    vía Microsoft Graph cuando esté disponible) - para señalar la causa real (permisos, licencias,
    o un tenant demasiado nuevo).

    Se escribe un archivo .log con marca de tiempo en la carpeta actual.

.PARÁMETRO AdminUpn
    UPN de administrador (por ejemplo, admin@clientdomain.com). Opcional en Windows (inicio de sesión interactivo si se omite).

.PARÁMETRO Device
    Forzar inicio de sesión con código de dispositivo. Opcional: Cloud Shell inicia sesión automáticamente sin ello.

.EJEMPLO
    .\Repair-PurviewAuditLogging.ps1 -AdminUpn admin@clientdomain.com
#>

[CmdletBinding()]
param(
    [string]$AdminUpn,
    [switch]$Device
)

$ErrorActionPreference = 'Stop'
$SupportEmail = 'protect@stoik.io'

# Paciencia ante los errores transitorios de Microsoft de "intenta de nuevo más tarde".
$RetryCount   = 8
$RetryWaitSec = 30   # 8 x 30s = hasta ~4 min por alternancia

# --- Utilidades de consola --------------------------------------------------------
function Write-Step { param($m) Write-Host "`n==> $m" -ForegroundColor Cyan }
function Write-Ok   { param($m) Write-Host "    [OK]   $m" -ForegroundColor Green }
function Write-Info { param($m) Write-Host "    [..]   $m" -ForegroundColor Gray }
function Write-Warn2{ param($m) Write-Host "    [WARN] $m" -ForegroundColor Yellow }
function Write-Err  { param($m) Write-Host "    [FAIL] $m" -ForegroundColor Red }

# --- Detección de entorno + verificación de requisito previo ------------------------------
# Cloud Shell establece una de estas; Windows PowerShell 5.1 no tiene $IsWindows, así que también
# aceptamos la edición Desktop.
$IsCloudShell = [bool]$env:ACC_CLOUD `
                -or ($env:POWERSHELL_DISTRIBUTION_CHANNEL -like '*CloudShell*') `
                -or ($env:AZUREPS_HOST_ENVIRONMENT -like '*cloud-shell*')
$OnWindows    = $IsWindows -or ($PSVersionTable.PSEdition -eq 'Desktop')

$exoVersion = (Get-Module -ListAvailable -Name ExchangeOnlineManagement |
               Sort-Object Version -Descending | Select-Object -First 1).Version

Write-Step "Entorno"
Write-Host "    SO / plataforma : $([System.Environment]::OSVersion.VersionString)"
Write-Host "    PowerShell    : $($PSVersionTable.PSVersion) ($($PSVersionTable.PSEdition))"
Write-Host "    Contexto      : $(if ($IsCloudShell) {'Azure Cloud Shell'} elseif ($OnWindows) {'Windows'} else {'no compatible'})"
Write-Host "    Módulo EXO    : $(if ($exoVersion) {$exoVersion} else {'no instalado'})"

if (-not ($OnWindows -or $IsCloudShell)) {
    Write-Err "Entorno no compatible."
    Write-Host "    Este script solo se ejecuta en Windows PowerShell o en Azure Cloud Shell." -ForegroundColor Yellow
    Write-Host "    Vuelve a ejecutarlo desde una máquina Windows, o desde https://shell.azure.com (PowerShell)." -ForegroundColor Yellow
    exit 2
}

# --- Registro ----------------------------------------------------------------
$timestamp = Get-Date -Format 'yyyyMMdd-HHmmss'
$LogFile   = Join-Path (Get-Location) "purview-repair-$timestamp.log"
Start-Transcript -Path $LogFile -Append | Out-Null

# Escribe texto en un archivo secundario sin lanzar nunca errores (el .log queda en la transcripción).
function Save-ErrorDetail {
    param($Text)
    if ([string]::IsNullOrWhiteSpace($Text)) { return $null }
    try {
        $f = [System.IO.Path]::ChangeExtension($LogFile, 'error.txt')
        "DETALLE COMPLETO DEL ERROR (para soporte)`n$Text" | Out-File -FilePath $f -ErrorAction Stop
        return $f
    } catch { return $null }
}

$connected = $false

# Ejecuta Set-AdminAuditLogConfig una vez y clasifica el resultado. Los cmdlets REST de EXO no
# siempre respetan try/catch, así que inspeccionamos -ErrorVariable.
function Set-AuditIngestionOnce {
    param([bool]$Value)
    $ErrorActionPreference = 'Continue'
    # Oculta el aviso bruto "...hasta 60 minutos para aplicarlo" de la consola (aún lo capturamos
    # mediante -WarningVariable e imprimimos nuestro propio [OK]). Esto es una preferencia, no una decoración
    # de comando ni una redirección de flujo.
    $WarningPreference = 'SilentlyContinue'
    $err = $null; $warn = $null
    # IMPORTANTE: invócalo EXACTAMENTE como un comando escrito a mano. Decorar la llamada con
    # -WarningAction SilentlyContinue / 2>$null / | Out-Null hace que EXO devuelva un (403) espurio
    # dentro de un script, mientras que el comando desnudo funciona. -ErrorVariable / -WarningVariable solo
    # capturan los flujos; no cambian cómo el cmdlet se comunica con el servidor.
    $null = Set-AdminAuditLogConfig -UnifiedAuditLogIngestionEnabled $Value `
        -ErrorVariable err -WarningVariable warn
    $errText  = ($err  | ForEach-Object { $_.ToString() }) -join ' '
    $warnText = ($warn | ForEach-Object { $_.ToString() }) -join ' '
    if ($errText) { $script:LastFullError = $errText }

    $dehydrated = ($errText -match 'Enable-OrganizationCustomization')
    $serverSide = ($errText -match 'server side error|try again after some time')
    $forbidden  = (-not $serverSide) -and ($errText -match '403|Forbidden|Interdit|Access is denied|not authorized|insufficient')

    if     ($dehydrated) { $short = 'El tenant está deshidratado (necesita Enable-OrganizationCustomization).' }
    elseif ($forbidden)  { $short = '403 Prohibido - problema de permisos / rol / licencias.' }
    elseif ($serverSide) { $short = "Microsoft está ocupado (error del lado del servidor, 'intenta de nuevo más tarde')." }
    elseif ($errText)    { $short = ($errText -split "`n")[0] }
    else                 { $short = '' }

    [pscustomobject]@{
        Ok = [string]::IsNullOrWhiteSpace($errText); Warning = $warnText; Error = $errText
        Short = $short; Dehydrated = $dehydrated; Forbidden = $forbidden; ServerSide = $serverSide
    }
}

# Alterna la configuración, reintentando solo en errores transitorios.
function Invoke-AuditToggle {
    param([bool]$Value, [string]$Label)
    $r = $null
    for ($i = 1; $i -le $RetryCount; $i++) {
        $r = Set-AuditIngestionOnce $Value
        if ($r.Ok)         { Write-Ok "$Label aceptado por Microsoft."; return $r }
        if ($r.Dehydrated) { return $r }                       # el llamador hidrata y luego reintenta
        if ($r.Forbidden)  { Write-Warn2 "$Label rechazado (permisos / rol / licencias) - no se reintentará."; return $r }

        Write-Warn2 "$Label falló (intento $i/$RetryCount): $($r.Short)"
        if ($i -lt $RetryCount) {
            Write-Info "Reintentando $Label en ${RetryWaitSec}s."
            Start-Sleep -Seconds $RetryWaitSec
        }
    }
    return $r
}

# Lista de comprobación manual mostrada cuando Graph no puede verificar automáticamente la suscripción / licencia / antigüedad del tenant.
function Write-ManualChecklist {
    Write-Info "Verifica manualmente en el centro de administración de Microsoft 365 (admin.microsoft.com):"
    Write-Info "  - Facturación > Tus productos : la suscripción está Activa (no suspendida/vencida)."
    Write-Info "  - Hay asignada una licencia que incluye Exchange Online (la mayoría de los planes M365/O365)."
    Write-Info "  - Fecha de configuración : los tenants creados en las últimas ~72h pueden no tener aún el registro de auditoría aprovisionado."
}

# Diagnóstico completo cuando la activación se rechaza de verdad.
function Invoke-FailureDiagnostic {
    param($ErrInfo)
    Write-Step "Diagnóstico - ¿por qué se rechazó la activación?"
    if ($ErrInfo -and $ErrInfo.Short) { Write-Info "Error informado : $($ErrInfo.Short)" }

    # Identidad conectada
    $upn = $null
    try {
        $ci  = Get-ConnectionInformation -ErrorAction Stop | Select-Object -First 1
        $upn = $ci.UserPrincipalName
        Write-Info "Conectado como : $upn"
        Write-Info "Id del tenant  : $($ci.TenantId)"
    } catch { Write-Info "No se pudo leer la información de conexión." }

    # Configuración del tenant / organización
    try {
        $org = Get-OrganizationConfig -ErrorAction Stop
        Write-Info "Organización   : $($org.DisplayName) [$($org.Name)]"
        Write-Info "IsDehydrated   : $($org.IsDehydrated)$(if ($org.IsDehydrated) {'  <- ejecuta Enable-OrganizationCustomization'})"
    } catch { Write-Info "No se pudo leer Get-OrganizationConfig." }

    # Permisos: ¿la cuenta tiene un rol que pueda configurar la auditoría?
    if ($upn) {
        try {
            $roles = Get-ManagementRoleAssignment -RoleAssignee $upn -Delegating:$false -ErrorAction Stop |
                     Select-Object -ExpandProperty Role -Unique
            Write-Info "Roles          : $([string]::Join(', ', $roles))"
            if ($roles -contains 'Audit Logs' -or $roles -contains 'Organization Configuration') {
                Write-Ok "La cuenta tiene un rol que permite la configuración de auditoría."
            } else {
                Write-Warn2 "La cuenta no tiene 'Organization Configuration' / 'Audit Logs'."
                Write-Warn2 "=> Añádela al grupo de roles 'Organization Management' (centro de administración de Exchange)."
            }
        } catch { Write-Info "No se pudieron enumerar los roles ($($_.Exception.Message))." }
    }

    # Suscripción / licencias / antigüedad del tenant vía Microsoft Graph (mejor esfuerzo, no fatal).
    if (Get-Command Get-MgOrganization -ErrorAction SilentlyContinue) {
        try {
            if (-not (Get-MgContext -ErrorAction SilentlyContinue)) {
                Write-Info "Conectando a Microsoft Graph (solo lectura) para comprobar suscripción/licencia/antigüedad..."
                Connect-MgGraph -Scopes 'Organization.Read.All' -NoWelcome -ErrorAction Stop
            }
            $o = Get-MgOrganization -ErrorAction Stop | Select-Object -First 1
            if ($o.CreatedDateTime) {
                $age = (New-TimeSpan -Start $o.CreatedDateTime -End (Get-Date)).Days
                Write-Info "Tenant creado : $($o.CreatedDateTime) (hace ~ $age días)"
                if ($age -lt 3) { Write-Warn2 "El tenant es muy nuevo (<3 días) - es posible que la provisión de auditoría aún no esté lista. Reintenta más tarde." }
            }
            foreach ($s in (Get-MgSubscribedSku -ErrorAction Stop)) {
                Write-Info "Licencia       : $($s.SkuPartNumber)  estado=$($s.CapabilityStatus)  plazas=$($s.PrepaidUnits.Enabled)"
                if ($s.CapabilityStatus -in 'Suspended','Deleted') {
                    Write-Warn2 "La suscripción '$($s.SkuPartNumber)' está $($s.CapabilityStatus) - no está activa."
                }
            }
        } catch {
            Write-Info "Comprobación de Graph no disponible: $($_.Exception.Message)"
            Write-ManualChecklist
        }
    } else {
        Write-Info "El módulo de Microsoft Graph no está instalado - no se puede comprobar automáticamente la suscripción/licencia/antigüedad del tenant."
        Write-ManualChecklist
    }
}

try {
    # --- Requisitos previos -----------------------------------------------------
    Write-Step "Comprobando requisitos previos"
    if (-not $exoVersion) {
        Write-Warn2 "No se encontró ExchangeOnlineManagement. Instalándolo para el usuario actual."
        if ($OnWindows -and $PSVersionTable.PSEdition -eq 'Desktop') {
            # Windows PowerShell 5.1 a menudo necesita TLS 1.2 explícitamente para llegar a PSGallery.
            [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
        }
        Install-Module -Name ExchangeOnlineManagement -Scope CurrentUser -Force -AllowClobber
        Write-Ok "Módulo instalado."
    } else {
        Write-Ok "ExchangeOnlineManagement presente ($exoVersion)."
    }
    Import-Module ExchangeOnlineManagement

    # --- Conectar -----------------------------------------------------------
    Write-Step "Conectando a Exchange Online"
    # En Azure Cloud Shell, el módulo EXO reutiliza automáticamente la identidad de la sesión - SIN dispositivo
    # código. Forza el código de dispositivo solo cuando el usuario lo solicite explícitamente con -Device.
    $useDevice = [bool]$Device
    if ($useDevice -and $AdminUpn) {
        Write-Warn2 "Inicio de sesión con código de dispositivo: abre la URL que aparece abajo e introduce el código."
        Connect-ExchangeOnline -UserPrincipalName $AdminUpn -Device -ShowBanner:$false
    } elseif ($useDevice) {
        Write-Warn2 "Inicio de sesión con código de dispositivo: abre la URL que aparece abajo e introduce el código."
        Connect-ExchangeOnline -Device -ShowBanner:$false
    } elseif ($AdminUpn) {
        Connect-ExchangeOnline -UserPrincipalName $AdminUpn -ShowBanner:$false
    } else {
        Connect-ExchangeOnline -ShowBanner:$false
    }
    $connected = $true
    Write-Ok "Conectado."

    # --- Diagnóstico --------------------------------------------------------
    Write-Step "Estado actual"
    $enabled      = (Get-AdminAuditLogConfig).UnifiedAuditLogIngestionEnabled
    $isDehydrated = (Get-OrganizationConfig).IsDehydrated
    Write-Host "    UnifiedAuditLogIngestionEnabled : $enabled"
    Write-Host "    IsDehydrated                    : $isDehydrated"

    # --- Deshabilitar primero solo si actualmente está ACTIVADO (el ciclo off->on desbloquea el portal) -------
    if ($enabled) {
        Write-Step "Paso 1 - Deshabilitando la ingesta (para forzar un ciclo limpio off->on)"
        $d = Invoke-AuditToggle -Value $false -Label "Deshabilitar"
        if (-not $d.Ok) { Write-Warn2 "La deshabilitación no se confirmó ($($d.Short)) - continuando con la reactivación de todos modos." }
    } else {
        Write-Info "El registro de auditoría está actualmente DESACTIVADO - habilitando directamente (no hace falta deshabilitar)."
    }

    # --- Habilitar ------------------------------------------------------------
    Write-Step "Paso 2 - Habilitando la ingesta"
    $e = Invoke-AuditToggle -Value $true -Label "Habilitar"
    if ($e.Dehydrated) {
        Write-Warn2 "Tenant deshidratado. Ejecutando Enable-OrganizationCustomization."
        try { Enable-OrganizationCustomization } catch { Write-Warn2 $_.Exception.Message }
        $h = 0
        do {
            Start-Sleep -Seconds 15
            $isDehydrated = (Get-OrganizationConfig).IsDehydrated
            $h++
            Write-Info "Esperando la hidratación... intento $h (IsDehydrated = $isDehydrated)"
        } while ($isDehydrated -and $h -lt 8)
        if ($isDehydrated) { throw "El tenant sigue deshidratado después de esperar. Vuelve a ejecutarlo en unos minutos." }
        Tenant hidratado.
        $e = Invoke-AuditToggle -Value $true -Label "Rehabilitar"
    }

    # --- Resultado ------------------------------------------------------------
    Write-Step "Resultado"
    Start-Sleep -Seconds 5
    $final = (Get-AdminAuditLogConfig).UnifiedAuditLogIngestionEnabled
    Write-Host "    UnifiedAuditLogIngestionEnabled (lectura nueva) : $final"

    # La lectura nueva es la verdad de referencia: si es True, el registro de auditoría ESTÁ habilitado, incluso cuando la
    # última llamada de habilitación dejó $e.Ok false (403 transitorio, ruido de "intenta de nuevo" en el flujo de errores).
    if ($final) {
        Write-Ok "El registro de auditoría ESTÁ HABILITADO. Hecho."
    }
    elseif ($e.Ok) {
        Write-Warn2 "La reactivación fue aceptada pero sigue leyendo False - normal: Microsoft puede tardar hasta"
        Write-Host  "    ~60 minutos en aplicarlo. Vuelve a comprobarlo con el comando de abajo en breve." -ForegroundColor Yellow
    }
    else {
        Write-Err "La activación NO fue aceptada por Microsoft. Ejecutando un diagnóstico."
        Save-ErrorDetail $script:LastFullError | Out-Null
        Invoke-FailureDiagnostic $e
    }

    # --- Verificar más tarde ------------------------------------------------------
    Write-Step "Verificar más tarde (reconéctate primero, el script se desconecta al final)"
    if ($AdminUpn) {
        Write-Host  "        Connect-ExchangeOnline -UserPrincipalName $AdminUpn -ShowBanner:`$false" -ForegroundColor Gray
    } else {
        Write-Host  "        Connect-ExchangeOnline -ShowBanner:`$false" -ForegroundColor Gray
    }
    Write-Host  "        Get-AdminAuditLogConfig | Format-List UnifiedAuditLogIngestionEnabled" -ForegroundColor Gray
    Write-Host  "    'True' significa que está habilitado."
}
catch {
    Write-Err "Algo salió mal: $($_.Exception.Message)"
    if ($connected) { try { Invoke-FailureDiagnostic $null } catch { } }
    $errorFile = Save-ErrorDetail $script:LastFullError
    Write-Host ""
    Write-Host "    >> Si necesitas ayuda, envía el archivo de registro a $SupportEmail :" -ForegroundColor Yellow
    Write-Host "       $LogFile" -ForegroundColor Yellow
    if ($errorFile) { Write-Host "       $errorFile" -ForegroundColor Yellow }
}
finally {
    if ($connected) {
        Write-Step "Desconectando"
        try { Disconnect-ExchangeOnline -Confirm:$false } catch { }
        Write-Ok "Desconectado."
    }
    Write-Host "`nRegistro guardado en: $LogFile" -ForegroundColor Gray
    Stop-Transcript | Out-Null
}
```

{% endcode %}


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.stoik.io/help/help-center-en-es/email-security/microsoft-365/purview-script.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
