> 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-fr/email-security/microsoft-365/purview-script.md).

# Script Purview

La version automatisée de [Erreurs d’activation de Purview](/help/help-center-fr/email-security/microsoft-365/purview-errors.md). Il se connecte à Exchange Online, force d’abord une désactivation puis une réactivation de l’ingestion du journal d’audit, et confirme le résultat. Si le tenant est déshydraté, il exécute `Enable-OrganizationCustomization`, attend la fin, puis réactive la journalisation d’audit. Si l’activation est réellement refusée, il affiche un diagnostic (rôles du compte, hydratation du tenant, état de la licence et de l’abonnement).

## Prérequis

* **Windows PowerShell** ou **Azure Cloud Shell** (mode PowerShell). Le script refuse de s’exécuter sur macOS/Linux, où le module Exchange Online échoue sur les opérations d’écriture.
* Un compte administrateur du tenant.

{% hint style="info" %}
Pas de machine Windows ? Si le tenant a un abonnement Azure actif, exécutez-le depuis [Azure Cloud Shell](https://shell.azure.com) en mode PowerShell : téléversez le script et exécutez-le sans `-AdminUpn`. Cloud Shell réutilise votre session, donc il n’y a pas d’invite de connexion. Purview lui-même n’exige pas d’abonnement Azure.
{% endhint %}

## Exécutez le script

{% stepper %}
{% step %}

### Copiez le script

Copiez la [script](#script) avec le bouton de copie en haut à droite du bloc de code, puis enregistrez-le sous `Repair-PurviewAuditLogging.ps1`.
{% endstep %}

{% step %}

### Autoriser l’exécution du script

Dans PowerShell, exécutez ceci une fois par fenêtre (uniquement pour la session, rien de permanent) :

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

{% endstep %}

{% step %}

### Exécutez-le avec le compte administrateur

Remplacez `<admin@clientdomain.com>` avec un administrateur du tenant. Une fenêtre de connexion du navigateur s’ouvre ; le script fait le reste.

```powershell
cd C:\path\to\the\folder
.\Repair-PurviewAuditLogging.ps1 -AdminUpn <admin@clientdomain.com>
```

{% endstep %}

{% step %}

### Vérifier plus tard

`Faux` juste après l’exécution est **normal**: Microsoft l’applique en quelques minutes (jusqu’à environ 60 min). Reconnectez-vous pour vérifier :

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

`Vrai` signifie que c’est terminé.
{% endstep %}
{% endstepper %}

## Dépannage

Le script lance un diagnostic et vous indique dans quel cas vous vous trouvez. Envoyez le `.log` qu’il a créé à <protect@stoik.io>.

* **Tenant déshydraté**: traité automatiquement. Le script exécute `Enable-OrganizationCustomization`, attend l’hydratation, puis réactive la journalisation d’audit.
* **403 Forbidden / refusé**: un vrai refus. Le compte n’a pas le bon rôle (le **Journaux d’audit** rôle, accordé via Organization Management ou Compliance Management), ou le tenant n’a aucune licence incluant Exchange Online.
* **Erreur côté serveur**: un « réessayez plus tard » transitoire de Microsoft. Le script réessaie déjà ; si cela persiste, relancez-le quelques minutes plus tard.

## Script

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

```powershell
<#
.RÉSUMÉ
    Répare la journalisation d’audit unifiée Microsoft Purview en forçant une désactivation puis une réactivation.

.DESCRIPTION
    PRÉREQUIS : exécutez ceci dans Windows PowerShell (5.1 ou 7+) OU dans Azure Cloud Shell.
    Le script refuse de s’exécuter ailleurs (macOS / Linux de bureau) - ces environnements
    rencontrent des particularités du module Exchange Online et ne sont pas pris en charge.

      - Windows      -> passez -AdminUpn (ou omettez-le pour une connexion interactive).
      - Cloud Shell  -> la connexion est automatique (le module EXO réutilise votre identité Cloud Shell).
                        Utilisez -Device uniquement si vous souhaitez explicitement une connexion par code appareil.

    Même lorsque la journalisation d’audit est déjà indiquée comme activée, le portail Purview peut rester bloqué sur
    son erreur. Forcer une désactivation puis une réactivation est ce qui la débloque.

    La réactivation est asynchrone : Microsoft peut prendre jusqu’à environ 60 minutes pour la refléter, donc un
    nouvel affichage peut encore montrer Faux juste après un appel réussi. C’est normal.

    Si l’activation est réellement refusée, le script lance un diagnostic : compte connecté &
    rôles, état d’hydratation du tenant, ancienneté du tenant et état de l’abonnement/de la licence (les deux derniers
    via Microsoft Graph lorsque disponible) - pour identifier la vraie cause (autorisations, licence,
    ou un tenant trop récent).

    Un fichier .log horodaté est écrit dans le dossier courant.

.PARAMÈTRE AdminUpn
    UPN administrateur (par ex. admin@clientdomain.com). Facultatif sur Windows (connexion interactive si omis).

.PARAMÈTRE Device
    Force la connexion par code appareil. Facultatif - Cloud Shell se connecte automatiquement sans cela.

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

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

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

# Patience pour les erreurs transitoires « réessayez plus tard » de Microsoft.
$RetryCount   = 8
$RetryWaitSec = 30   # 8 x 30 s = jusqu’à ~4 min par bascule

# --- Aides à la console --------------------------------------------------------
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 }

# --- Détection de l’environnement + contrôle des prérequis ------------------------------
# Cloud Shell définit l’un de ceux-ci ; Windows PowerShell 5.1 n’a pas $IsWindows, donc nous
# acceptons aussi l’édition 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 "Environnement"
Write-Host "    OS / plateforme : $([System.Environment]::OSVersion.VersionString)"
Write-Host "    PowerShell    : $($PSVersionTable.PSVersion) ($($PSVersionTable.PSEdition))"
Write-Host "    Contexte       : $(if ($IsCloudShell) {'Azure Cloud Shell'} elseif ($OnWindows) {'Windows'} else {'non pris en charge'})"
Write-Host "    Module EXO    : $(if ($exoVersion) {$exoVersion} else {'non installé'})"

if (-not ($OnWindows -or $IsCloudShell)) {
    Environnement non pris en charge.
    Write-Host "    Ce script ne s’exécute que dans Windows PowerShell ou dans Azure Cloud Shell." -ForegroundColor Yellow
    Write-Host "    Relancez-le depuis une machine Windows, ou depuis https://shell.azure.com (PowerShell)." -ForegroundColor Yellow
    exit 2
}

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

# Écrit du texte dans un fichier secondaire sans jamais lever d’exception (le .log est conservé par la transcription).
function Save-ErrorDetail {
    param($Text)
    if ([string]::IsNullOrWhiteSpace($Text)) { return $null }
    try {
        $f = [System.IO.Path]::ChangeExtension($LogFile, 'error.txt')
        "DÉTAIL COMPLET DE L’ERREUR (pour le support)`n$Text" | Out-File -FilePath $f -ErrorAction Stop
        return $f
    } catch { return $null }
}

$connected = $false

# Exécute Set-AdminAuditLogConfig une fois et classe le résultat. Les cmdlets REST EXO n’honorent pas
# toujours try/catch, donc nous inspectons -ErrorVariable.
function Set-AuditIngestionOnce {
    param([bool]$Value)
    $ErrorActionPreference = 'Continue'
    # Masque l’avertissement brut « ... jusqu’à 60 minutes pour appliquer » dans la console (nous le capturons quand même
    # via -WarningVariable et affichons notre propre [OK]). Il s’agit d’une préférence, pas d’une décoration
    # de commande ni d’une redirection de flux.
    $WarningPreference = 'SilentlyContinue'
    $err = $null; $warn = $null
    # IMPORTANT : invoquez ceci EXACTEMENT comme une commande tapée à la main. Décorer l’appel avec
    # -WarningAction SilentlyContinue / 2>$null / | Out-Null fait renvoyer à EXO un faux (403)
    # à l’intérieur d’un script, alors que la commande nue fonctionne. -ErrorVariable / -WarningVariable
    # capturent seulement les flux ; ils ne modifient pas la manière dont le cmdlet parle au serveur.
    $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 = 'Le tenant est déshydraté (nécessite Enable-OrganizationCustomization).' }
    elseif ($forbidden)  { $short = '403 Forbidden - problème d’autorisations / de rôle / de licence.' }
    elseif ($serverSide) { $short = "Microsoft est occupé (erreur côté serveur, « réessayez plus tard »)." }
    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
    }
}

# Bascule le paramètre, en réessayant uniquement en cas d’erreurs transitoires.
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 accepté par Microsoft."; return $r }
        if ($r.Dehydrated) { return $r }                       # l’appelant hydrate puis réessaie
        if ($r.Forbidden)  { Write-Warn2 "$Label refusé (autorisations / rôle / licence) - pas de nouvel essai."; return $r }

        Write-Warn2 "$Label a échoué (tentative $i/$RetryCount) : $($r.Short)"
        if ($i -lt $RetryCount) {
            Write-Info "Nouvelle tentative pour $Label dans ${RetryWaitSec}s."
            Start-Sleep -Seconds $RetryWaitSec
        }
    }
    return $r
}

# Liste de vérification manuelle affichée lorsque Graph ne peut pas vérifier automatiquement l’abonnement / la licence / l’ancienneté du tenant.
function Write-ManualChecklist {
    Write-Info "Vérifiez manuellement dans le centre d’administration Microsoft 365 (admin.microsoft.com) :"
    Write-Info "  - Facturation > Vos produits : l’abonnement est Actif (pas suspendu/expiré)."
    Write-Info "  - Une licence incluant Exchange Online est attribuée (la plupart des offres M365/O365)."
    Write-Info "  - Date de création : les tenants créés au cours des ~72 dernières heures peuvent ne pas encore avoir l’audit provisionné."
}

# Diagnostic complet lorsque l’activation est réellement refusée.
function Invoke-FailureDiagnostic {
    param($ErrInfo)
    Write-Step "Diagnostic - pourquoi l’activation a-t-elle été refusée ?"
    if ($ErrInfo -and $ErrInfo.Short) { Write-Info "Erreur signalée : $($ErrInfo.Short)" }

    # Identité connectée
    $upn = $null
    try {
        $ci  = Get-ConnectionInformation -ErrorAction Stop | Select-Object -First 1
        $upn = $ci.UserPrincipalName
        Write-Info "Connecté en tant que : $upn"
        Write-Info "ID du tenant      : $($ci.TenantId)"
    } catch { Write-Info "Impossible de lire les informations de connexion." }

    # Configuration du tenant / de l’organisation
    try {
        $org = Get-OrganizationConfig -ErrorAction Stop
        Write-Info "Organisation   : $($org.DisplayName) [$($org.Name)]"
        Write-Info "IsDehydrated   : $($org.IsDehydrated)$(if ($org.IsDehydrated) {'  <- exécutez Enable-OrganizationCustomization'})"
    } catch { Write-Info "Impossible de lire Get-OrganizationConfig." }

    # Autorisations : le compte possède-t-il un rôle pouvant définir la configuration d’audit ?
    if ($upn) {
        try {
            $roles = Get-ManagementRoleAssignment -RoleAssignee $upn -Delegating:$false -ErrorAction Stop |
                     Select-Object -ExpandProperty Role -Unique
            Write-Info "Rôles          : $([string]::Join(', ', $roles))"
            if ($roles -contains 'Audit Logs' -or $roles -contains 'Organization Configuration') {
                Write-Ok "Le compte possède un rôle autorisant la configuration d’audit."
            } else {
                Write-Warn2 "Le compte ne possède pas 'Organization Configuration' / 'Audit Logs'."
                Write-Warn2 "=> Ajoutez-le au groupe de rôles 'Organization Management' (centre d’administration Exchange)."
            }
        } catch { Write-Info "Impossible d’énumérer les rôles ($($_.Exception.Message))." }
    }

    # Abonnement / licence / ancienneté du tenant via Microsoft Graph (au mieux, non bloquant).
    if (Get-Command Get-MgOrganization -ErrorAction SilentlyContinue) {
        try {
            if (-not (Get-MgContext -ErrorAction SilentlyContinue)) {
                Write-Info "Connexion à Microsoft Graph (lecture seule) pour vérifier l’abonnement / la licence / l’ancienneté..."
                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 créé : $($o.CreatedDateTime) (il y a environ $age jours)"
                if ($age -lt 3) { Write-Warn2 "Le tenant est très récent (<3 jours) - le provisionnement de l’audit n’est peut-être pas prêt. Réessayez plus tard." }
            }
            foreach ($s in (Get-MgSubscribedSku -ErrorAction Stop)) {
                Write-Info "Licence        : $($s.SkuPartNumber)  status=$($s.CapabilityStatus)  seats=$($s.PrepaidUnits.Enabled)"
                if ($s.CapabilityStatus -in 'Suspended','Deleted') {
                    Write-Warn2 "L’abonnement '$($s.SkuPartNumber)' est $($s.CapabilityStatus) - inactif."
                }
            }
        } catch {
            Write-Info "Vérification Graph indisponible : $($_.Exception.Message)"
            Write-ManualChecklist
        }
    } else {
        Write-Info "Le module Microsoft Graph n’est pas installé - impossible de vérifier automatiquement l’abonnement / la licence / l’ancienneté du tenant."
        Write-ManualChecklist
    }
}

try {
    # --- Prérequis -----------------------------------------------------
    Write-Step "Vérification des prérequis"
    if (-not $exoVersion) {
        Write-Warn2 "ExchangeOnlineManagement introuvable. Installation pour l’utilisateur actuel."
        if ($OnWindows -and $PSVersionTable.PSEdition -eq 'Desktop') {
            # Windows PowerShell 5.1 a souvent besoin de TLS 1.2 explicitement pour accéder à PSGallery.
            [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
        }
        Install-Module -Name ExchangeOnlineManagement -Scope CurrentUser -Force -AllowClobber
        Write-Ok "Module installé."
    } else {
        Write-Ok "ExchangeOnlineManagement présent ($exoVersion)."
    }
    Import-Module ExchangeOnlineManagement

    # --- Connexion -----------------------------------------------------------
    Write-Step "Connexion à Exchange Online"
    # Dans Azure Cloud Shell, le module EXO réutilise automatiquement l’identité de session - PAS de
    # code appareil. N’imposez le code appareil que si l’utilisateur le demande explicitement avec -Device.
    $useDevice = [bool]$Device
    if ($useDevice -and $AdminUpn) {
        Write-Warn2 "Connexion par code appareil : ouvrez l’URL affichée ci-dessous et saisissez le code."
        Connect-ExchangeOnline -UserPrincipalName $AdminUpn -Device -ShowBanner:$false
    } elseif ($useDevice) {
        Write-Warn2 "Connexion par code appareil : ouvrez l’URL affichée ci-dessous et saisissez le code."
        Connect-ExchangeOnline -Device -ShowBanner:$false
    } elseif ($AdminUpn) {
        Connect-ExchangeOnline -UserPrincipalName $AdminUpn -ShowBanner:$false
    } else {
        Connect-ExchangeOnline -ShowBanner:$false
    }
    $connected = $true
    Write-Ok "Connecté."

    # --- Diagnostic --------------------------------------------------------
    Write-Step "État actuel"
    $enabled      = (Get-AdminAuditLogConfig).UnifiedAuditLogIngestionEnabled
    $isDehydrated = (Get-OrganizationConfig).IsDehydrated
    Write-Host "    UnifiedAuditLogIngestionEnabled : $enabled"
    Write-Host "    IsDehydrated                    : $isDehydrated"

    # --- Désactiver d’abord uniquement si c’est actuellement ACTIVÉ (le cycle off->on débloque le portail) -------
    if ($enabled) {
        Write-Step "Étape 1 - Désactivation de l’ingestion (pour forcer un cycle propre désactivation->réactivation)"
        $d = Invoke-AuditToggle -Value $false -Label "Désactivation"
        if (-not $d.Ok) { Write-Warn2 "La désactivation n’a pas été confirmée ($($d.Short)) - on continue quand même vers la réactivation." }
    } else {
        Write-Info "La journalisation d’audit est actuellement DÉSACTIVÉE - activation directe (aucune désactivation nécessaire)."
    }

    # --- Réactivation ------------------------------------------------------------
    Write-Step "Étape 2 - Activation de l’ingestion"
    $e = Invoke-AuditToggle -Value $true -Label "Activation"
    if ($e.Dehydrated) {
        Write-Warn2 "Tenant déshydraté. Exécution de Enable-OrganizationCustomization."
        try { Enable-OrganizationCustomization } catch { Write-Warn2 $_.Exception.Message }
        $h = 0
        do {
            Start-Sleep -Seconds 15
            $isDehydrated = (Get-OrganizationConfig).IsDehydrated
            $h++
            Write-Info "En attente de l’hydratation... tentative $h (IsDehydrated = $isDehydrated)"
        } while ($isDehydrated -and $h -lt 8)
        if ($isDehydrated) { throw "Le tenant est toujours déshydraté après l’attente. Relancez dans quelques minutes." }
        Write-Ok "Tenant hydraté."
        $e = Invoke-AuditToggle -Value $true -Label "Réactivation"
    }

    # --- Résultat ------------------------------------------------------------
    Write-Step "Résultat"
    Start-Sleep -Seconds 5
    $final = (Get-AdminAuditLogConfig).UnifiedAuditLogIngestionEnabled
    Write-Host "    UnifiedAuditLogIngestionEnabled (lecture fraîche) : $final"

    # La lecture fraîche est la vérité terrain : si elle vaut True, la journalisation d’audit EST activée, même lorsque
    # le dernier appel d’activation a laissé $e.Ok faux (403 transitoire, bruit « réessayez » sur le flux d’erreur).
    if ($final) {
        Write-Ok "La journalisation d’audit est ACTIVÉE. Terminé."
    }
    elseif ($e.Ok) {
        Write-Warn2 "La réactivation a été acceptée mais la valeur est toujours False - normal : Microsoft peut prendre jusqu’à"
        Write-Host  "    ~60 minutes pour l’appliquer. Revérifiez bientôt avec la commande ci-dessous." -ForegroundColor Yellow
    }
    else {
        Write-Err "L’activation n’a PAS été acceptée par Microsoft. Lancement d’un diagnostic."
        Save-ErrorDetail $script:LastFullError | Out-Null
        Invoke-FailureDiagnostic $e
    }

    # --- Vérifier plus tard ------------------------------------------------------
    Write-Step "Vérifier plus tard (reconnectez-vous d’abord, le script se déconnecte à la fin)"
    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' signifie que c’est activé."
}
catch {
    Write-Err "Quelque chose a mal tourné : $($_.Exception.Message)"
    if ($connected) { try { Invoke-FailureDiagnostic $null } catch { } }
    $errorFile = Save-ErrorDetail $script:LastFullError
    Write-Host ""
    Write-Host "    >> Si vous avez besoin d’aide, envoyez le fichier journal à $SupportEmail :" -ForegroundColor Yellow
    Write-Host "       $LogFile" -ForegroundColor Yellow
    if ($errorFile) { Write-Host "       $errorFile" -ForegroundColor Yellow }
}
finally {
    if ($connected) {
        Write-Step "Déconnexion en cours"
        try { Disconnect-ExchangeOnline -Confirm:$false } catch { }
        Write-Ok "Déconnecté."
    }
    Write-Host "`nJournal enregistré dans : $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-fr/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.
