public/Connect-Msec.ps1

function Connect-Msec {
    <#
    .SYNOPSIS
        Establishes a Microsoft Security session bound to a certificate in Azure Key Vault.

    .DESCRIPTION
        Use this once per PowerShell session before calling any Get-Msec* function. It:
          1. Requires you to be signed into Azure (Connect-AzAccount) - that's your *own*
             identity and is what Key Vault sees for RBAC and audit.
          2. Reads the certificate's metadata from Key Vault. New-MsecApp stamped the cert
             with two tags - AppId and TenantId - so the caller normally only needs the
             Key Vault and certificate names.
          3. Stores tenant + client IDs, vault name, key name, and thumbprint in a
             module-scoped session.
          4. Acquires the Graph and Defender tokens up front - each acquisition signs a
             fresh JWT client assertion inside Key Vault via Invoke-AzKeyVaultKeyOperation.
             The private key never leaves the vault.

        Identity resolution order:
          ClientId -> -ClientId param -> AppId tag on the cert -> error
          TenantId -> -TenantId param -> TenantId tag on the cert -> Get-AzContext tenant

        Required Azure RBAC for the calling user (or group):
          - 'Key Vault Certificate User' on the vault (to read cert metadata + tags).
          - 'Key Vault Crypto User' on the vault (to sign with the key).

        A SUCCESSFUL CONNECTION IS REMEMBERED for the tenant, so a later
        Select-MsecAzureContext into it reconnects the app session on its own rather than
        leaving the two identities pointing at different tenants. Vault name, client id and
        certificate name only - no secret, because there isn't one to store: signing happens
        inside Key Vault. Written only after the tokens have been obtained, so a saved profile
        always describes a connection that actually worked. -NoSave skips it.

    .PARAMETER NoSave
        Do not remember this connection for Select-MsecAzureContext to replay.
    .PARAMETER KeyVaultName
        The Azure Key Vault containing the certificate.
    .PARAMETER CertificateName
        The name of the certificate in Key Vault. Defaults to 'msec-app' (matches the
        default used by New-MsecApp).
    .PARAMETER ClientId
        Optional override. App registration (client) ID. Only needed if the cert was not
        tagged by New-MsecApp.
    .PARAMETER TenantId
        Optional override. Entra ID tenant ID. Only needed if the cert was not tagged by
        New-MsecApp AND the current Az context tenant is wrong.

    .EXAMPLE
        Connect-AzAccount
        Connect-Msec -KeyVaultName 'kv-mysec'
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string] $KeyVaultName,
        [Parameter()][string] $CertificateName = 'msec-app',
        [Parameter()][string] $ClientId,
        [Parameter()][string] $TenantId,

        # Do not remember this connection for Select-MsecAzureContext to reuse.
        [Parameter()][switch] $NoSave
    )

    $azCtx = Get-AzContext -ErrorAction SilentlyContinue
    if (-not $azCtx) {
        throw 'No Azure context. Run Connect-AzAccount before Connect-Msec.'
    }

    $meta = Get-MsecCertificateMetadata -VaultName $KeyVaultName -CertificateName $CertificateName

    # Resolve ClientId: explicit param wins, else cert tag.
    if (-not $ClientId) { $ClientId = $meta.AppId }
    if (-not $ClientId) {
        throw "Could not resolve ClientId. The certificate '$CertificateName' has no 'AppId' tag and -ClientId was not provided. If the cert was created by New-MsecApp, the tag should be present; otherwise pass -ClientId explicitly."
    }

    # Resolve TenantId: explicit param > cert tag > Az context.
    if (-not $TenantId) { $TenantId = $meta.TenantId }
    if (-not $TenantId) { $TenantId = $azCtx.Tenant.Id }

    # Resolve the cloud's endpoints from the Az context (commercial, China, US Gov).
    # Pins the whole session to one cloud; switching clouds means a fresh Connect-AzAccount
    # + Connect-Msec, because a single Az session is one cloud at a time.
    $endpoints = Get-MsecEnvironment

    $script:MsecSession = @{
        TenantId        = $TenantId
        ClientId        = $ClientId
        KeyVaultName    = $KeyVaultName
        KeyName         = $meta.KeyName
        ThumbprintBytes = $meta.ThumbprintBytes
        Endpoints       = $endpoints
        Tokens          = @{}
    }

    # Prime tokens so configuration errors surface here, not deep in a Get-Msec* call.
    # Graph exists in every cloud; Defender (securitycenter) is commercial-only, so prime
    # it only where it has an endpoint (e.g. it is retired in Azure China).
    [void](Get-MsecAccessToken -Resource $endpoints.GraphResource)
    if ($endpoints.DefenderResource) {
        [void](Get-MsecAccessToken -Resource $endpoints.DefenderResource)
    }
    else {
        Write-Verbose "Defender (securitycenter) has no endpoint in $($endpoints.EnvironmentName); skipping Defender token. Defender functions will be unavailable."
    }

    # Remembered so Select-MsecAzureContext can reconnect this tenant on its own. Written only
    # after the tokens above succeeded, so a profile always describes a connection that worked
    # rather than one that was merely typed.
    if (-not $NoSave) {
        Save-MsecTenantProfile -TenantId $TenantId -KeyVaultName $KeyVaultName `
                               -ClientId $ClientId -CertificateName $CertificateName
    }

    Write-Verbose "Connected to tenant $TenantId as app $ClientId in $($endpoints.EnvironmentName) (cert: $($meta.Thumbprint))"
}