Microsoft Purview PowerShell script Report only

Export-PurviewSensitivityLabels.ps1

Documents Microsoft Purview sensitivity labels and label policies to CSV and JSON.

Overview#

Connects to Security & Compliance PowerShell and reads every sensitivity label (Get-Label) and label policy (Get-LabelPolicy). For each label the script resolves the parent label name, parses the LabelActions JSON to report whether encryption, content marking or group/site protection is configured, and lists the distinct action types. For each policy it flattens the Settings pairs and surfaces mandatory labelling, the default label (resolved to its display name) and the downgrade-justification setting. Writes SensitivityLabels.csv, LabelPolicies.csv plus the raw objects as SensitivityLabels.json and LabelPolicies.json into -OutputFolder. The script is read-only.

Safety: Report only — makes no changes to your tenant. Run Get-Help .\Export-PurviewSensitivityLabels.ps1 -Full for the complete help text.

Parameters#

ParameterWhat it does
-OutputFolderFolder that receives the four export files. Defaults to .\PurviewLabelsExport_yyyyMMdd-HHmm\ (created if missing).
-PassThruAlso emit the shaped label objects (one per sensitivity label) to the pipeline.

Examples#

PowerShell
PS> .\Export-PurviewSensitivityLabels.ps1

Exports labels and policies to .\PurviewLabelsExport_<timestamp>\ and prints a summary.

PowerShell
PS> .\Export-PurviewSensitivityLabels.ps1 -OutputFolder C:\Docs\Purview\Labels -Verbose

Exports into a fixed folder - handy for keeping a dated copy of the label taxonomy in source control.

PowerShell
PS> .\Export-PurviewSensitivityLabels.ps1 -PassThru | Where-Object { $_.EncryptionEnabled } | Select-Object DisplayName, ParentLabel, Priority

Lists the labels that apply encryption.

Permissions, modules and notes#

Author : Omer Eltayeb Blog : https://www.oeltayeb.com GitHub : https://github.com/omer-eltayeb Version : 1.0.0 Requires : PowerShell 5.1 or 7.x, ExchangeOnlineManagement 3.x Permissions : Compliance Administrator, Compliance Data Administrator, Information Protection Reader or Global Reader Notes : Opens a Security & Compliance PowerShell session (Connect-IPPSSession), not an Exchange Online one. Government clouds need Connect-IPPSSession -ConnectionUri for their endpoint before running the script. LabelActions only lists the configured action types; run Get-Label -IncludeDetailedLabelActions when you need the full per-action settings (the raw JSON export already contains everything the cmdlet returns).

Full source#

PowerShell · Export-PurviewSensitivityLabels.ps1
<#
.SYNOPSIS
    Documents Microsoft Purview sensitivity labels and label policies to CSV and JSON.
.DESCRIPTION
    Connects to Security & Compliance PowerShell and reads every sensitivity label (Get-Label) and label policy
    (Get-LabelPolicy). For each label the script resolves the parent label name, parses the LabelActions JSON to
    report whether encryption, content marking or group/site protection is configured, and lists the distinct
    action types. For each policy it flattens the Settings pairs and surfaces mandatory labelling, the default
    label (resolved to its display name) and the downgrade-justification setting.
    Writes SensitivityLabels.csv, LabelPolicies.csv plus the raw objects as SensitivityLabels.json and
    LabelPolicies.json into -OutputFolder. The script is read-only.
.PARAMETER OutputFolder
    Folder that receives the four export files. Defaults to .\PurviewLabelsExport_yyyyMMdd-HHmm\ (created if missing).
.PARAMETER PassThru
    Also emit the shaped label objects (one per sensitivity label) to the pipeline.
.EXAMPLE
    PS> .\Export-PurviewSensitivityLabels.ps1
    Exports labels and policies to .\PurviewLabelsExport_<timestamp>\ and prints a summary.
.EXAMPLE
    PS> .\Export-PurviewSensitivityLabels.ps1 -OutputFolder C:\Docs\Purview\Labels -Verbose
    Exports into a fixed folder - handy for keeping a dated copy of the label taxonomy in source control.
.EXAMPLE
    PS> .\Export-PurviewSensitivityLabels.ps1 -PassThru | Where-Object { $_.EncryptionEnabled } | Select-Object DisplayName, ParentLabel, Priority
    Lists the labels that apply encryption.
.NOTES
    Author      : Omer Eltayeb
    Blog        : https://www.oeltayeb.com
    GitHub      : https://github.com/omer-eltayeb
    Version     : 1.0.0
    Requires    : PowerShell 5.1 or 7.x, ExchangeOnlineManagement 3.x
    Permissions : Compliance Administrator, Compliance Data Administrator, Information Protection Reader or Global Reader
    Notes       : Opens a Security & Compliance PowerShell session (Connect-IPPSSession), not an Exchange Online one.
                  Government clouds need Connect-IPPSSession -ConnectionUri for their endpoint before running the script.
                  LabelActions only lists the configured action types; run Get-Label -IncludeDetailedLabelActions when you
                  need the full per-action settings (the raw JSON export already contains everything the cmdlet returns).
.LINK
    https://learn.microsoft.com/powershell/module/exchange/get-label
.LINK
    https://learn.microsoft.com/powershell/module/exchange/get-labelpolicy
#>
#Requires -Version 5.1
#Requires -Modules ExchangeOnlineManagement

[CmdletBinding()]
param(
    [Parameter()]
    [string]$OutputFolder,

    [Parameter()]
    [switch]$PassThru
)

$ErrorActionPreference = 'Stop'

#region Helpers
function Connect-ExchangeIfNeeded {
    <# Connects to Exchange Online (or Security & Compliance PowerShell) only when no live session exists. #>
    [CmdletBinding()]
    param(
        [Parameter()]
        [switch]$Compliance
    )
    $connections = @(Get-ConnectionInformation -ErrorAction SilentlyContinue)
    if ($Compliance) {
        $active = @($connections | Where-Object { $_.ConnectionUri -like '*compliance*' -and $_.State -eq 'Connected' })
        if ($active.Count -eq 0) {
            Write-Verbose 'Connecting to Security & Compliance PowerShell.'
            Connect-IPPSSession -ErrorAction Stop
        }
    }
    else {
        $active = @($connections | Where-Object { $_.ConnectionUri -notlike '*compliance*' -and $_.State -eq 'Connected' })
        if ($active.Count -eq 0) {
            Write-Verbose 'Connecting to Exchange Online.'
            Connect-ExchangeOnline -ShowBanner:$false -ErrorAction Stop
        }
    }
}

function ConvertTo-SettingsTable {
    <# Converts the "[key, value]" strings found in the Settings property of labels and policies into a hashtable. #>
    [CmdletBinding()]
    param(
        [Parameter()]
        [AllowNull()]
        $Settings
    )
    $table = @{}
    foreach ($entry in @($Settings)) {
        if ($null -eq $entry) { continue }
        if ([string]$entry -match '^\s*\[\s*([^,\]]+?)\s*,\s*(.*?)\s*\]\s*$') {
            $table[$Matches[1]] = $Matches[2]
        }
    }
    return $table
}

function ConvertTo-SettingsString {
    <# Flattens a settings hashtable into "key=value; key=value" sorted by key. #>
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [hashtable]$Table
    )
    return (($Table.Keys | Sort-Object | ForEach-Object { '{0}={1}' -f $_, $Table[$_] }) -join '; ')
}

function ConvertTo-LocationString {
    <# Joins a policy location collection with ';', collapsing to 'All' when the collection contains All. #>
    [CmdletBinding()]
    param(
        [Parameter()]
        [AllowNull()]
        $Location
    )
    $names = New-Object -TypeName System.Collections.Generic.List[string]
    foreach ($item in @($Location)) {
        if ($null -eq $item) { continue }
        $name = $null
        if ($null -ne $item.PSObject.Properties['Name']) { $name = [string]$item.Name }
        if ([string]::IsNullOrWhiteSpace($name)) { $name = [string]$item }
        if (-not [string]::IsNullOrWhiteSpace($name)) { $names.Add($name) }
    }
    if ($names.Count -eq 0) { return $null }
    if ($names -contains 'All') { return 'All' }
    return ($names -join ';')
}

function Export-JsonFile {
    <# Serialises the raw objects to UTF-8 JSON without a BOM so any tooling can consume the file. #>
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [AllowEmptyCollection()]
        [object[]]$InputObject,

        [Parameter(Mandatory = $true)]
        [string]$Path
    )
    $json = ConvertTo-Json -InputObject @($InputObject) -Depth 10
    [System.IO.File]::WriteAllText($Path, $json, (New-Object -TypeName System.Text.UTF8Encoding -ArgumentList $false))
}
#endregion Helpers

#region Main
if ([string]::IsNullOrWhiteSpace($OutputFolder)) {
    $OutputFolder = Join-Path -Path (Get-Location).Path -ChildPath ('PurviewLabelsExport_{0}' -f (Get-Date -Format 'yyyyMMdd-HHmm'))
}
if (-not (Test-Path -Path $OutputFolder)) {
    New-Item -Path $OutputFolder -ItemType Directory -Force | Out-Null
}
$OutputFolder = (Resolve-Path -Path $OutputFolder).Path

try {
    Connect-ExchangeIfNeeded -Compliance
}
catch {
    throw "Unable to connect to Security & Compliance PowerShell: $($_.Exception.Message)"
}

Write-Verbose 'Retrieving sensitivity labels.'
try {
    $labels = @(Get-Label -ErrorAction Stop)
}
catch {
    throw "Failed to retrieve sensitivity labels: $($_.Exception.Message)"
}
Write-Verbose 'Retrieving label policies.'
try {
    $policies = @(Get-LabelPolicy -ErrorAction Stop)
}
catch {
    throw "Failed to retrieve label policies: $($_.Exception.Message)"
}

# Guid -> DisplayName lookup used for parent labels and policy default labels.
$labelNameById = @{}
foreach ($label in $labels) {
    if ($null -ne $label.Guid) { $labelNameById[[string]$label.Guid] = [string]$label.DisplayName }
}

$labelRows = New-Object -TypeName System.Collections.Generic.List[object]
foreach ($label in ($labels | Sort-Object -Property Priority)) {
    $actionTypes = New-Object -TypeName System.Collections.Generic.List[string]
    foreach ($action in @($label.LabelActions)) {
        if ([string]::IsNullOrWhiteSpace([string]$action)) { continue }
        try {
            $parsed = [string]$action | ConvertFrom-Json -ErrorAction Stop
            $type = [string]$parsed.Type
            if (-not [string]::IsNullOrWhiteSpace($type) -and -not $actionTypes.Contains($type.ToLowerInvariant())) {
                $actionTypes.Add($type.ToLowerInvariant())
            }
        }
        catch {
            Write-Warning "Label '$($label.DisplayName)': could not parse a LabelActions entry: $($_.Exception.Message)"
        }
    }

    $parentId = [string]$label.ParentId
    $isSubLabel = (-not [string]::IsNullOrWhiteSpace($parentId) -and $parentId -ne [guid]::Empty.ToString())
    $parentLabel = $null
    if ($isSubLabel) {
        if ($labelNameById.ContainsKey($parentId)) { $parentLabel = $labelNameById[$parentId] } else { $parentLabel = $parentId }
    }
    $labelSettings = ConvertTo-SettingsTable -Settings $label.Settings

    $labelRows.Add([PSCustomObject]@{
            DisplayName           = [string]$label.DisplayName
            Name                  = [string]$label.Name
            Guid                  = [string]$label.Guid
            Priority              = $label.Priority
            IsSubLabel            = $isSubLabel
            ParentLabel           = $parentLabel
            ParentId              = $(if ($isSubLabel) { $parentId } else { $null })
            ContentType           = (@($label.ContentType) -join ';')
            Workload              = (@($label.Workload) -join ';')
            Disabled              = [bool]$label.Disabled
            EncryptionEnabled     = ($actionTypes -contains 'encrypt')
            ContentMarkingEnabled = (($actionTypes -contains 'applycontentmarking') -or ($actionTypes -contains 'applywatermarking') -or ($actionTypes -contains 'applydynamicwatermarking'))
            GroupSiteProtection   = (($actionTypes -contains 'protectgroup') -or ($actionTypes -contains 'protectsite'))
            ActionTypes           = ($actionTypes -join ';')
            Tooltip               = [string]$label.Tooltip
            Settings              = ConvertTo-SettingsString -Table $labelSettings
            WhenCreated           = $label.WhenCreated
            WhenChanged           = $label.WhenChanged
        })
}

$policyRows = New-Object -TypeName System.Collections.Generic.List[object]
foreach ($policy in ($policies | Sort-Object -Property Name)) {
    $settings = ConvertTo-SettingsTable -Settings $policy.Settings
    $defaultLabelId = [string]$settings['defaultlabelid']
    $defaultLabel = $null
    if (-not [string]::IsNullOrWhiteSpace($defaultLabelId) -and $defaultLabelId -ne 'None') {
        if ($labelNameById.ContainsKey($defaultLabelId)) { $defaultLabel = $labelNameById[$defaultLabelId] } else { $defaultLabel = $defaultLabelId }
    }
    $policyLabels = @($policy.Labels | ForEach-Object { [string]$_ })

    $policyRows.Add([PSCustomObject]@{
            Name                          = [string]$policy.Name
            Guid                          = [string]$policy.Guid
            Enabled                       = [bool]$policy.Enabled
            Mode                          = [string]$policy.Mode
            LabelCount                    = $policyLabels.Count
            Labels                        = ($policyLabels -join ';')
            Mandatory                     = ([string]$settings['mandatory'] -eq 'True')
            DefaultLabel                  = $defaultLabel
            DefaultLabelId                = $(if ([string]::IsNullOrWhiteSpace($defaultLabelId)) { $null } else { $defaultLabelId })
            RequireDowngradeJustification = ([string]$settings['requiredowngradejustification'] -eq 'True')
            ExchangeLocation              = ConvertTo-LocationString -Location $policy.ExchangeLocation
            SharePointLocation            = ConvertTo-LocationString -Location $policy.SharePointLocation
            OneDriveLocation              = ConvertTo-LocationString -Location $policy.OneDriveLocation
            ModernGroupLocation           = ConvertTo-LocationString -Location $policy.ModernGroupLocation
            Settings                      = ConvertTo-SettingsString -Table $settings
            WhenCreated                   = $policy.WhenCreated
            WhenChanged                   = $policy.WhenChanged
        })
}

$labelCsv = Join-Path -Path $OutputFolder -ChildPath 'SensitivityLabels.csv'
$policyCsv = Join-Path -Path $OutputFolder -ChildPath 'LabelPolicies.csv'
if ($labelRows.Count -gt 0) { $labelRows | Export-Csv -Path $labelCsv -NoTypeInformation -Encoding UTF8 }
if ($policyRows.Count -gt 0) { $policyRows | Export-Csv -Path $policyCsv -NoTypeInformation -Encoding UTF8 }
try {
    Export-JsonFile -InputObject $labels -Path (Join-Path -Path $OutputFolder -ChildPath 'SensitivityLabels.json')
    Export-JsonFile -InputObject $policies -Path (Join-Path -Path $OutputFolder -ChildPath 'LabelPolicies.json')
}
catch {
    Write-Warning "CSV files were written but the raw JSON export failed: $($_.Exception.Message)"
}

$subLabelCount = @($labelRows | Where-Object { $_.IsSubLabel }).Count
$encryptedCount = @($labelRows | Where-Object { $_.EncryptionEnabled }).Count
$disabledCount = @($labelRows | Where-Object { $_.Disabled }).Count
$enabledPolicyCount = @($policyRows | Where-Object { $_.Enabled }).Count
$mandatoryPolicyCount = @($policyRows | Where-Object { $_.Mandatory }).Count

Write-Host ''
Write-Host 'Sensitivity label export summary' -ForegroundColor Cyan
Write-Host ('  Labels                 : {0} ({1} top-level, {2} sub-labels)' -f $labelRows.Count, ($labelRows.Count - $subLabelCount), $subLabelCount)
Write-Host ('  Labels with encryption : {0}' -f $encryptedCount)
Write-Host ('  Labels disabled        : {0}' -f $disabledCount)
Write-Host ('  Label policies         : {0} ({1} enabled, {2} mandatory)' -f $policyRows.Count, $enabledPolicyCount, $mandatoryPolicyCount)
Write-Host ('  Output folder          : {0}' -f $OutputFolder)

if ($PassThru) {
    $labelRows
}
#endregion Main

Scripts are provided as-is under the MIT licence. Review the permissions a script requests, test in a non-production tenant, and use -WhatIf before letting any script change anything.

Found a bug or have an improvement? Open an issue on GitHub or email me.