public/Connect-MsecPurview.ps1

function Connect-MsecPurview {
    <#
    .SYNOPSIS
        Opens an app-only Security & Compliance PowerShell session for the Get-MsecPurview*
        commands.

    .DESCRIPTION
        CALLING THIS IS OPTIONAL. The Get-MsecPurview* commands open a session themselves on
        first use, so Connect-Msec is normally all you need. Use this directly when the tenant
        domain has to be given explicitly, or to choose when a few hundred compliance cmdlet
        names are imported into your runspace. Measured: 102 cmdlets, none of them clashing with
        the ExchangeOnlineManagement module's own exports - so the import is bulk rather than
        destructive, and a Get- command doing it unasked is still a side effect worth knowing about.

        Purview's configuration is not in Microsoft Graph. Retention labels have a v1.0 endpoint
        and eDiscovery cases have one, but DLP policies, DLP rules, sensitivity label actions and
        label policies do not - the only complete source is Security & Compliance PowerShell,
        which is why this exists rather than another Invoke-MsecGraphRequest caller.

        NO NEW CONSENT IS NEEDED. Connect-IPPSSession accepts -AccessToken and -AppId, the same
        shape Connect-MsecExchangeOnline uses, so the existing Key Vault certificate reaches the
        compliance endpoint as the app. The resource differs
        (ps.compliance.protection.outlook.com rather than outlook.office365.com) but the identity
        and the trust do not.

        The app still needs a directory role to be allowed in - Global Reader or Compliance
        Administrator. New-MsecApp assigns Global Reader when asked for -Workload Exchange, and a
        403 here almost always means that assignment is missing rather than that a permission is.

        -Organization is optional: left off, the tenant's default verified domain is read from
        Graph, which the app can already do.

    .PARAMETER Organization
        Tenant domain, e.g. contoso.onmicrosoft.com. Resolved from Graph when omitted.

    .PARAMETER ShowBanner
        Show the ExchangeOnlineManagement banner. Suppressed by default.

    .EXAMPLE
        Connect-Msec -KeyVaultName kv-msec
        Get-MsecPurviewDlpPolicy # connects by itself

        # Explicit, when the domain must be given or the import timed deliberately:
        Connect-MsecPurview -Organization contoso.onmicrosoft.com

    .NOTES
        Needs the ExchangeOnlineManagement module, which is not a dependency of msec - only the
        Exchange and Purview commands require it.

        NB this shares cmdlet names with an Exchange Online session. Connecting both into one
        runspace lets the later connection win for overlapping names; connect Purview in its own
        session if you also need Get-Mailbox.
    #>

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

        [Parameter()]
        [switch] $ShowBanner
    )

    Assert-MsecSession

    if (-not (Get-Module -ListAvailable -Name ExchangeOnlineManagement)) {
        throw 'ExchangeOnlineManagement is required for Connect-MsecPurview. Install with: Install-Module ExchangeOnlineManagement -Scope CurrentUser'
    }
    Import-Module ExchangeOnlineManagement -ErrorAction Stop

    $resource = 'https://ps.compliance.protection.outlook.com'
    $environment = $script:MsecSession.Endpoints.EnvironmentName
    if ($environment -and $environment -ne 'AzureCloud') {
        Write-Warning "The msec session is in '$environment'. Connect-MsecPurview assumes the commercial compliance endpoint ($resource), which is probably wrong for this cloud."
    }

    if (-not $Organization) {
        $Organization = Get-MsecTenantDomain
        Write-Verbose "Resolved organization from Graph: $Organization"
    }

    $token = Get-MsecAccessToken -Resource $resource

    $connectParams = @{
        AccessToken  = $token
        Organization = $Organization
        AppId        = $script:MsecSession.ClientId
        ErrorAction  = 'Stop'
    }
    if (-not $ShowBanner) { $connectParams['ShowBanner'] = $false }

    try {
        Connect-IPPSSession @connectParams
    }
    catch {
        $detail = $_.Exception.Message
        if ($detail -match '401|403|[Uu]nauthor|[Ff]orbidden|AADSTS') {
            throw ("Rejected by the compliance endpoint as app $($script:MsecSession.ClientId). This is usually a missing " +
                   'DIRECTORY ROLE rather than a missing API permission - the app needs Global Reader or Compliance ' +
                   "Administrator, assigned in Entra ID > Roles and administrators. Original error: $detail")
        }
        throw $detail
    }

    Write-Verbose "Purview connected to $Organization as app $($script:MsecSession.ClientId) (app-only)."
}