GraphAppToolkit.psm1

#Region '.\Classes\TkEmailAppParams.ps1' -1

class TkEmailAppParams {
    [string]$AppId
    [string]$Id
    [string]$AppName
    [string]$CertificateSubject
    [string]$AppRestrictedSendGroup
    [string]$CertExpires
    [string]$CertThumbprint
    [string]$ConsentUrl
    [string]$DefaultDomain
    [string]$SendAsUser
    [string]$SendAsUserEmail
    [string]$TenantID
    # Constructor
    TkEmailAppParams(
        [string]$AppId,
        [string]$Id,
        [string]$AppName,
        [string]$CertificateSubject,
        [string]$AppRestrictedSendGroup,
        [string]$CertExpires,
        [string]$CertThumbprint,
        [string]$ConsentUrl,
        [string]$DefaultDomain,
        [string]$SendAsUser,
        [string]$SendAsUserEmail,
        [string]$TenantID
    ) {
        $this.AppId                  = $AppId
        $this.Id                     = $Id
        $this.AppName                = $AppName
        $this.CertificateSubject     = $CertificateSubject
        $this.AppRestrictedSendGroup = $AppRestrictedSendGroup
        $this.CertExpires            = $CertExpires
        $this.CertThumbprint         = $CertThumbprint
        $this.ConsentUrl             = $ConsentUrl
        $this.DefaultDomain          = $DefaultDomain
        $this.SendAsUser             = $SendAsUser
        $this.SendAsUserEmail        = $SendAsUserEmail
        $this.TenantID               = $TenantID
    }
    # (Optional) A helper method that converts CertExpires to a DateTime object
    [DateTime] GetCertExpiresAsDateTime() {
        return [DateTime]::Parse($this.CertExpires)
    }
}
#EndRegion '.\Classes\TkEmailAppParams.ps1' 47
#Region '.\Classes\TkM365AuditAppParams.ps1' -1

class TkM365AuditAppParams {
    [string]$AppName
    [string]$AppId
    [string]$ObjectId
    [string]$TenantId
    [string]$CertThumbprint
    [string]$CertExpires
    [string]$ConsentUrl
    [string]$MgGraphPermissions
    [string]$SharePointPermissions
    [string]$ExchangePermissions
    # Constructor
    TkM365AuditAppParams(
        [string]$AppName,
        [string]$AppId,
        [string]$ObjectId,
        [string]$TenantId,
        [string]$CertThumbprint,
        [string]$CertExpires,
        [string]$ConsentUrl,
        [string]$MgGraphPermissions,
        [string]$SharePointPermissions,
        [string]$ExchangePermissions
    ) {
        $this.AppName               = $AppName
        $this.AppId                 = $AppId
        $this.ObjectId              = $ObjectId
        $this.TenantId              = $TenantId
        $this.CertThumbprint        = $CertThumbprint
        $this.CertExpires           = $CertExpires
        $this.ConsentUrl            = $ConsentUrl
        $this.MgGraphPermissions    = $MgGraphPermissions
        $this.SharePointPermissions = $SharePointPermissions
        $this.ExchangePermissions   = $ExchangePermissions
    }
    [DateTime] GetCertExpiresAsDateTime() {
        return [DateTime]::Parse($this.CertExpires)
    }
    # (Optional) Helper methods to split space-delimited permissions into arrays:
    [string[]]GetMgGraphPermissionsArray() {
        return $this.MgGraphPermissions -split '\s+'
    }
    [string[]]GetSharePointPermissionsArray() {
        return $this.SharePointPermissions -split '\s+'
    }
    [string[]]GetExchangePermissionsArray() {
        return $this.ExchangePermissions -split '\s+'
    }
}
#EndRegion '.\Classes\TkM365AuditAppParams.ps1' 50
#Region '.\Classes\TkMemPolicyManagerAppParams .ps1' -1

class TkMemPolicyManagerAppParams  {
    [string]$AppId
    [string]$AppName
    [string]$CertThumbprint
    [string]$ObjectId
    [string]$ConsentUrl
    [string]$PermissionSet
    [string]$Permissions
    [string]$TenantId
    # Constructor
    TkMemPolicyManagerAppParams (
        [string]$AppId,
        [string]$AppName,
        [string]$CertThumbprint,
        [string]$ObjectId,
        [string]$ConsentUrl,
        [string]$PermissionSet,
        [string]$Permissions,
        [string]$TenantId
    ) {
        $this.AppId          = $AppId
        $this.AppName        = $AppName
        $this.CertThumbprint = $CertThumbprint
        $this.ObjectId       = $ObjectId
        $this.ConsentUrl     = $ConsentUrl
        $this.PermissionSet  = $PermissionSet
        $this.Permissions    = $Permissions
        $this.TenantId       = $TenantId
    }

    # (Optional) Helper method to split the Permissions string into an array:
    [string[]] GetPermissionsArray() {
        return $this.Permissions -split '\s+'
    }
}
#EndRegion '.\Classes\TkMemPolicyManagerAppParams .ps1' 36
#Region '.\Private\Connect-TkMsService.ps1' -1

<#
    .SYNOPSIS
    Connects to Microsoft Graph and/or Exchange Online services.
    .DESCRIPTION
    The Connect-TkMsService function establishes a connection to Microsoft Graph and/or Exchange Online services.
    It checks for existing sessions and reuses them if valid, otherwise, it creates new sessions.
    The function supports logging and provides detailed information about the connection process.
    .PARAMETER MgGraph
    Switch parameter to indicate if a connection to Microsoft Graph should be established.
    .PARAMETER GraphAuthScopes
    Array of strings specifying the scopes required for Microsoft Graph authentication.
    .PARAMETER ExchangeOnline
    Switch parameter to indicate if a connection to Exchange Online should be established.
    .EXAMPLE
    Connect-TkMsService -MgGraph -GraphAuthScopes @('User.Read', 'Mail.Read')
 
    This example connects to Microsoft Graph with the specified scopes.
    .EXAMPLE
    Connect-TkMsService -ExchangeOnline
 
    This example connects to Exchange Online.
    .EXAMPLE
    Connect-TkMsService -MgGraph -GraphAuthScopes @('User.Read', 'Mail.Read') -ExchangeOnline
 
    This example connects to both Microsoft Graph and Exchange Online.
    .NOTES
    This function requires the Microsoft.Graph and ExchangeOnlineManagement modules to be installed and imported.
#>

function Connect-TkMsService {
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')]
    param (
        [Parameter(
            HelpMessage = 'Switch to connect to Microsoft Graph.'
        )]
        [Switch]
        $MgGraph,
        [Parameter(
            HelpMessage = 'Array of scopes required for Microsoft Graph authentication.'
        )]
        [String[]]
        $GraphAuthScopes,
        [Parameter(
            HelpMessage = 'Switch to connect to Exchange Online.'
        )]
        [Switch]
        $ExchangeOnline
    )
    # Begin Logging
    if (-not $script:LogString) {
        Write-AuditLog -Start
    }
    else {
        Write-AuditLog -BeginFunction
    }
    Write-AuditLog '###############################################'
    #----------------------------------------------
    # Section 1: Microsoft Graph
    #----------------------------------------------
    if ($MgGraph) {
        $shouldProcessTarget = $GraphAuthScopes -join ', '
        $shouldProcessOperation = 'Connect-MgGraph'
        if ($PSCmdlet.ShouldProcess($shouldProcessTarget, $shouldProcessOperation)) {
            try {
                # 1) Attempt to see if we have a valid Graph session
                $graphIsValid = $false
                try {
                    # If this succeeds, presumably we have a valid token/context
                    Get-MgUser -Top 1 -ErrorAction Stop | Out-Null
                    $ContextMg = Get-MgContext -ErrorAction Stop
                    # Check required scopes
                    $scopesNeeded = $GraphAuthScopes
                    $missing = $scopesNeeded | Where-Object { $ContextMg.Scopes -notcontains $_ }
                    if ($missing) {
                        Write-AuditLog "The following needed scopes are missing: $($missing -join ', ')"
                    }
                    else {
                        Write-AuditLog 'An active Microsoft Graph session is detected and all required scopes are present.'
                        $graphIsValid = $true
                    }
                }
                catch {
                    # Either no session or it's invalid/expired
                    $graphIsValid = $false
                }
                # 2) If valid session, ask user if they want to reuse it
                if ($graphIsValid) {
                    $org = Get-MgOrganization -ErrorAction Stop
                    $shouldProcessTarget = 'Microsoft Graph'
                    $shouldProcessOperation = "Use existing session for Account: $($ContextMg.Account) Tenant: $($org.DisplayName) AuthType: $($ContextMg.AuthType)"
                    if ($PSCmdlet.ShouldProcess($shouldProcessTarget, $shouldProcessOperation)) {
                        Write-AuditLog 'Using existing Microsoft Graph session.'
                    }
                    else {
                        # Remove the old context so we can connect fresh
                        Remove-MgContext -ErrorAction SilentlyContinue
                        Write-AuditLog 'Creating a new Microsoft Graph session.'
                        Connect-MgGraph -ContextScope Process -Scopes $scopesNeeded  `
                            -ErrorAction Stop | Out-Null
                        Write-AuditLog 'Connected to Microsoft Graph.'
                    }
                }
                else {
                    # No valid session, so just connect
                    Write-AuditLog 'No valid Microsoft Graph session found. Connecting...'
                    Connect-MgGraph -ContextScope Process -Scopes $scopesNeeded `
                        -ErrorAction Stop | Out-Null
                    Write-AuditLog 'Connected to Microsoft Graph.'
                }
            }
            catch {
                Write-AuditLog -Severity Error -Message "Error connecting to Microsoft Graph. Error: $($_.Exception.Message)"
                throw
            }
        }
    }
    #----------------------------------------------
    # Section 2: Exchange Online
    #----------------------------------------------
    if ($ExchangeOnline) {
        $shouldProcessTarget = 'Connecting to Exchange Online using modern authentication pop-up.'
        $shouldProcessOperation = 'Connect-ExchangeOnline'
        if ($PSCmdlet.ShouldProcess($shouldProcessTarget, $shouldProcessOperation)) {
            try {
                # 1) Attempt to see if we have a valid Exchange session
                $exoIsValid = $false
                try {
                    $ExoOrg = Get-OrganizationConfig -ErrorAction Stop
                    $exoIsValid = $true
                }
                catch {
                    # Either no session or it's invalid/expired
                    $exoIsValid = $false
                }
                # 2) If valid session, ask user if they want to reuse it
                if ($exoIsValid) {
                    Write-AuditLog 'An active Exchange Online session is detected.'
                    Write-AuditLog "Tenant: `n$($ExoOrg.DisplayName)`n"
                    $shouldProcessTarget = 'ExchangeOnline'
                    $shouldProcessOperation = "Use existing session for Org: $($ExoOrg.DisplayName) OnMicrosoftId: $($ExoOrg.Name )"
                    if ($PSCmdlet.ShouldProcess($shouldProcessTarget, $shouldProcessOperation)) {
                        Write-AuditLog 'Using existing Exchange Online session.'
                    }
                    else {
                        # Disconnect old session
                        Disconnect-ExchangeOnline -Confirm:$false -ErrorAction SilentlyContinue
                        Write-AuditLog 'Creating new Exchange Online session.'
                        Connect-ExchangeOnline -ShowBanner:$false -ErrorAction Stop
                        Write-AuditLog 'Connected to Exchange Online.'
                    }
                }
                else {
                    Write-AuditLog 'No valid Exchange Online session found. Connecting...'
                    Connect-ExchangeOnline -ShowBanner:$false -ErrorAction Stop
                    Write-AuditLog 'Connected to Exchange Online.'
                }
            }
            catch {
                Write-AuditLog -Severity Error -Message "Error connecting to Exchange Online. Error: $($_.Exception.Message)"
                throw
            }
        }
    }
    Write-AuditLog -EndFunction
}
#EndRegion '.\Private\Connect-TkMsService.ps1' 165
#Region '.\Private\ConvertTo-ParameterSplat.ps1' -1

<#
    .SYNOPSIS
        Converts an object's properties to a parameter splatting hashtable script.
    .DESCRIPTION
        The ConvertTo-ParameterSplat function takes an input object and converts its properties into a PowerShell hashtable script that can be used for parameter splatting. This is useful for dynamically constructing parameter sets for cmdlets.
    .PARAMETER InputObject
        The object whose properties will be converted into a parameter splatting hashtable script. This parameter is mandatory and accepts input from the pipeline.
    .OUTPUTS
        System.String
        The function outputs a string that represents the hashtable script for parameter splatting.
    .EXAMPLE
        PS C:\> $obj = [PSCustomObject]@{ Name = "John"; Age = 30 }
        PS C:\> $obj | ConvertTo-ParameterSplat
        `$params = @{
            Name = "John"
            Age = 30
        }
    .NOTES
        Author: DrIOSx
        Last Updated: 2025-03-16
#>

function ConvertTo-ParameterSplat {
    [CmdletBinding()]
    [OutputType([string])]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, HelpMessage = 'The object whose properties will be converted into a parameter splatting hashtable script.')]
        [ValidateNotNullOrEmpty()]
        [PSObject]$InputObject
    )
    process {
        Write-AuditLog -Message "Starting ConvertTo-ParameterSplat function." -Severity "Information"

        $splatScript = "`$params = @{`n"
        $InputObject.psobject.Properties | ForEach-Object {
            $value = $_.Value
            if ($value -is [string]) {
                $value = "`"$value`""
            }
            $splatScript += " $($_.Name) = $value`n"
        }
        $splatScript += "}"

        Write-AuditLog -Message "Completed ConvertTo-ParameterSplat function." -Severity "Information"
        Write-Output $splatScript
    }
}
#EndRegion '.\Private\ConvertTo-ParameterSplat.ps1' 47
#Region '.\Private\Get-TkExistingCert.ps1' -1

<#
.SYNOPSIS
    Retrieves an existing certificate from the current user's certificate store based on the provided certificate name.
.DESCRIPTION
    The Get-TkExistingCert function searches for a certificate in the current user's "My" certificate store with a subject that matches the provided certificate name.
    If the certificate is found, it logs audit messages and provides instructions for removing the certificate if needed.
    If the certificate is not found, it logs an audit message indicating that the certificate does not exist.
.PARAMETER CertName
    The subject name of the certificate to search for in the current user's certificate store.
.EXAMPLE
    PS C:\> Get-TkExistingCert -CertName "CN=example.com"
    This command searches for a certificate with the subject "CN=example.com" in the current user's certificate store.
.NOTES
    Author: DrIOSx
    Date: 2025-03-12
    Version: 1.0
#>

function Get-TkExistingCert {
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')]
    param (
        [Parameter(Mandatory = $true, HelpMessage = 'The subject name of the certificate to search for in the current user''s certificate store.')]
        [string]$CertName
    )

    if (-not $script:LogString) {
        Write-AuditLog -Start
    }
    else {
        Write-AuditLog -BeginFunction
    }

    $ExistingCert = Get-ChildItem -Path Cert:\CurrentUser\My -ErrorAction SilentlyContinue |
    Where-Object { $_.Subject -eq $CertName } -ErrorAction SilentlyContinue

    if ( $ExistingCert) {
        $VerbosePreference = 'Continue'
        Write-AuditLog "Certificate with subject '$CertName' already exists in the certificate store."
        Write-AuditLog 'You can remove the old certificate if no longer needed with the following commands:'
        Write-AuditLog '1. Verify if more than one cert already exists:'
        Write-AuditLog "Get-ChildItem -Path Cert:\CurrentUser\My | Where-Object { `$_.Subject -eq '$CertName' }"
        Write-AuditLog '2. If you are comfortable removing the old certificate, and any duplicates, run the following command:'
        Write-AuditLog "Get-ChildItem -Path Cert:\CurrentUser\My | Where-Object { `$_.Subject -eq '$CertName' } | Remove-Item"
        Write-AuditLog 'If you would like to remove the certificate, confirm the operation when prompted.'
        $shouldProcessOperation = 'Remove-Item'
        $shouldProcessTarget = "Certificate with subject '$CertName' with thumbprint $($ExistingCert.Thumbprint)"
        if ($PSCmdlet.ShouldProcess($shouldProcessTarget, $shouldProcessOperation)) {
            Get-ChildItem -Path Cert:\CurrentUser\My | Where-Object { $_.Subject -eq $CertName } -ErrorAction Stop | Remove-Item
            Write-AuditLog "Certificate with subject '$CertName' removed."
        }
        else {
            Write-AuditLog "Certificate with subject '$CertName' not removed."
            throw "Certificate with subject '$CertName' was not removed by user."
        }
        $VerbosePreference = 'SilentlyContinue'
    }
    else {
        Write-AuditLog "Certificate with subject '$CertName' does not exist in the certificate store. Continuing..."
    }

    Write-AuditLog -EndFunction
}
#EndRegion '.\Private\Get-TkExistingCert.ps1' 62
#Region '.\Private\Get-TkExistingSecret.ps1' -1

<#
.SYNOPSIS
    Checks if a secret exists in the specified vault.
.DESCRIPTION
    The Get-TkExistingSecret function checks if a secret with the specified name exists in the specified vault.
    It uses the Get-Secret cmdlet to retrieve the secret and returns $true if the secret exists, otherwise $false.
    The default vault name is 'GraphEmailAppLocalStore'.
.PARAMETER AppName
    The name of the application for which the secret is being checked.
.PARAMETER VaultName
    The name of the vault where the secret is stored. Defaults to 'GraphEmailAppLocalStore'.
.OUTPUTS
    [bool] $true if the secret exists, otherwise $false.
.EXAMPLE
    $secretExists = Get-TkExistingSecret -AppName 'MyApp'
    if ($secretExists) {
        Write-Output "Secret exists."
    } else {
        Write-Output "Secret does not exist."
    }
.NOTES
    This function uses the Get-Secret cmdlet to check for the existence of a secret in the specified vault.
#>

function Get-TkExistingSecret {
    param (
        [string]$AppName,
        [string]$VaultName = 'GraphEmailAppLocalStore'
    )
    Write-AuditLog -BeginFunction
    try {
        $ExistingSecret = Get-Secret -Name "$AppName" -Vault $VaultName -ErrorAction SilentlyContinue
        if ($ExistingSecret) {
            return $true
        }
        else {
            return $false
        }
    }
    finally {
        Write-AuditLog -EndFunction
    }
}

#EndRegion '.\Private\Get-TkExistingSecret.ps1' 44
#Region '.\Private\Get-TkMsalToken.ps1' -1

<#
    .SYNOPSIS
        Retrieves an OAuth2 token for accessing Microsoft Graph or other APIs using various authentication methods.
    .DESCRIPTION
        The Get-TkMsalToken function supports three authentication methods:
        - Client Certificate
        - Client Secret
        - Managed Identity (only works in Azure-hosted environments)
    .PARAMETER ClientCertificate
        The X.509 certificate used for authentication. Example:
        $ClientCertificate = Get-Item Cert:\CurrentUser\My\<thumbprint>
    .PARAMETER ClientSecret
        The client secret used for authentication.
    .PARAMETER UseManagedIdentity
        Use Azure Managed Identity for authentication (only works in Azure-hosted environments).
    .PARAMETER ClientId
        The Azure AD application (client) ID.
    .PARAMETER TenantId
        The Azure AD tenant ID (GUID).
    .PARAMETER Scope
        The API scope for token access. Default is Microsoft Graph.
    .PARAMETER AuthorityType
        The authority type to use for authentication. Valid values are 'Global', 'AzureGov', and 'China'.
    .EXAMPLE
        Get-TkMsalToken -ClientCertificate $ClientCert -ClientId 'your-client-id' -TenantId 'your-tenant-id'
    .EXAMPLE
        Get-TkMsalToken -ClientSecret $ClientSecret -ClientId 'your-client-id' -TenantId 'your-tenant-id'
    .EXAMPLE
        Get-TkMsalToken -UseManagedIdentity -ClientId 'your-client-id' -TenantId 'your-tenant-id'
    .NOTES
        Author: DrIOSx
        Date: 2025-03-16
        Version: 1.0
#>

function Get-TkMsalToken {
    [CmdletBinding(DefaultParameterSetName = 'ClientCertificate')]
    [OutputType([string])]
    param (
        # Client Certificate
        [Parameter(
            ParameterSetName = 'ClientCertificate',
            Mandatory = $true,
            HelpMessage = `
                "The X.509 certificate used for authentication. Example: `n`$ClientCertificate = Get-Item Cert:\CurrentUser\My\<thumbprint>"
        )]
        [System.Security.Cryptography.X509Certificates.X509Certificate2]
        $ClientCertificate,
        # Client Secret
        [Parameter(
            ParameterSetName = 'ClientSecret',
            Mandatory = $true,
            HelpMessage = `
                'The client secret used for authentication.'
        )]
        [ValidateNotNullOrEmpty()]
        [SecureString]
        $ClientSecret,
        # Managed Identity
        [Parameter(
            ParameterSetName = 'ManagedIdentity',
            Mandatory = $true,
            HelpMessage = `
                'Use Azure Managed Identity for authentication (only works in Azure-hosted environments).'
        )]
        [switch]
        $UseManagedIdentity,
        # Client ID
        [Parameter(
            Mandatory = $true,
            HelpMessage = 'The Azure AD application (client) ID.'
        )]
        [ValidatePattern('^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$')]
        [string]
        $ClientId,
        # Tenant ID
        [Parameter(
            Mandatory = $true,
            HelpMessage = 'The Azure AD tenant ID (GUID).'
        )]
        [ValidatePattern('^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$')]
        [string]
        $TenantId,
        # Scope
        [Parameter(
            HelpMessage = 'The API scope for token access. Default is Microsoft Graph.'
        )]
        [ValidatePattern('^https:\/\/[a-zA-Z0-9.-]+\/[a-zA-Z0-9.-]+$')]
        [string]
        $Scope = 'https://graph.microsoft.com/.default',
        # Authority Type
        [Parameter(
            HelpMessage = 'The authority type to use for authentication.'
        )]
        [ValidateSet('Global', 'AzureGov', 'China')]
        [string]
        $AuthorityType = 'Global'
    )
    begin {
        if (-not $script:logString) {
            Write-AuditLog -Start
        }
        else {
            Write-AuditLog -BeginFunction
        }
        # Define Authority URL based on selected cloud type
        switch ($AuthorityType) {
            'Global' { $authority = "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token" }
            'AzureGov' { $authority = "https://login.microsoftonline.us/$TenantId/oauth2/v2.0/token" }
            'China' { $authority = "https://login.chinacloudapi.cn/$TenantId/oauth2/v2.0/token" }
        }
    }
    process {
        if ($PSCmdlet.ParameterSetName -eq 'ManagedIdentity') {
            # Managed Identity Authentication (Only Works in Azure-hosted Environments)
            try {
                $uri = 'http://169.254.169.254/metadata/identity/oauth2/token?resource=https://graph.microsoft.com&api-version=2019-08-01'
                $response = Invoke-RestMethod `
                    -Uri $uri `
                    -Method Get `
                    -Headers @{ 'Metadata' = 'true' } `
                    -ErrorAction Stop
                return $response.access_token
            }
            catch {
                Write-Error "Failed to obtain token via Managed Identity: $_"
                throw
            }
        }
        elseif ($PSCmdlet.ParameterSetName -eq 'ClientCertificate') {
            if ($ClientCertificate.NotAfter -lt (Get-Date)) {
                Write-Error "The provided certificate has expired on $($ClientCertificate.NotAfter). Please use a valid certificate."
                throw "Certificate has expired."
            }
            $jwtHeader = @{
                alg = 'RS256'
                typ = 'JWT'
                x5t = [Convert]::ToBase64String($ClientCertificate.GetCertHash()) -replace '\+', '-' -replace '/', '_' -replace '='
            }
            $iatTime = [int](Get-Date (Get-Date).ToUniversalTime() -UFormat %s)
            $expTime = $iatTime + 600  # 10 min expiration
            $jwtPayload = @{
                aud = $authority
                exp = $expTime
                iat = $iatTime
                nbf = $iatTime
                iss = $ClientId
                sub = $ClientId
                jti = [guid]::NewGuid().ToString()
            }
            $base64UrlEncode = { param ($string) [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($string)) -replace '\+', '-' -replace '/', '_' -replace '=' }
            $jwtHeaderEncoded = &$base64UrlEncode (ConvertTo-Json $jwtHeader -Compress)
            $jwtPayloadEncoded = &$base64UrlEncode (ConvertTo-Json $jwtPayload -Compress)
            $jwtToSign = "$jwtHeaderEncoded.$jwtPayloadEncoded"
            try {
                $csp = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($ClientCertificate)
                $signature = [Convert]::ToBase64String(
                    $csp.SignData(
                        [System.Text.Encoding]::UTF8.GetBytes($jwtToSign),
                        [System.Security.Cryptography.HashAlgorithmName]::SHA256,
                        [System.Security.Cryptography.RSASignaturePadding]::Pkcs1
                    )
                ) -replace '\+', '-' -replace '/', '_' -replace '='
            }
            catch {
                Write-Error "Failed to sign JWT: $_"
                throw
            }
            $clientAssertion = "$jwtToSign.$signature"
            $body = @{
                client_id             = $ClientId
                client_assertion      = $clientAssertion
                client_assertion_type = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer'
                grant_type            = 'client_credentials'
                scope                 = $Scope
            }
        }
        elseif ($PSCmdlet.ParameterSetName -eq 'ClientSecret') {
            $plainClientSecret = ConvertFrom-SecureString -SecureString $ClientSecret -AsPlainText
            $body = @{
                client_id     = $ClientId
                client_secret = $plainClientSecret
                grant_type    = 'client_credentials'
                scope         = $Scope
            }
        }
    }
    end {
        try {
            Write-AuditLog "Requesting token from $authority."
            $tokenResponse = (Invoke-RestMethod -Method Post -Uri $authority -ContentType 'application/x-www-form-urlencoded' -Body $body -ErrorAction Stop).access_token
            Write-AuditLog "Successfully obtained token from $authority."
            Write-AuditLog -EndFunction
            return $tokenResponse
        }
        catch {
            Write-AuditLog -Message "Failed to obtain token: $($_.Exception.Message)" -Severity "Error"
            throw
        }
    }
}
#EndRegion '.\Private\Get-TkMsalToken.ps1' 201
#Region '.\Private\Initialize-TkAppAuthCertificate.ps1' -1

<#
    .SYNOPSIS
    Initializes or retrieves an authentication certificate for the TkApp.
    .DESCRIPTION
    The Initialize-TkAppAuthCertificate function either retrieves an existing certificate by thumbprint or creates a new self-signed certificate if no thumbprint is provided. The function logs the process and supports ShouldProcess for confirmation prompts.
    .PARAMETER Thumbprint
    The thumbprint of the certificate to retrieve. If omitted, a new self-signed certificate is created.
    .PARAMETER AppName
    An optional name to store in the output object (e.g., the associated app name).
    .PARAMETER Subject
    The subject name for the new certificate if no thumbprint is provided. Default is 'CN=TkDefaultSelfSignedCert'.
    .PARAMETER CertStoreLocation
    The certificate store location (e.g., "Cert:\CurrentUser\My"). Default is 'Cert:\CurrentUser\My'.
    .PARAMETER KeyExportPolicy
    Exportable key policy for the new certificate. Valid values are 'Exportable' and 'NonExportable'. Default is 'NonExportable'.
    .OUTPUTS
    PSCustomObject
    An object containing the certificate thumbprint and expiration date. If AppName is provided, it is included in the output object.
    .EXAMPLE
    Initialize-TkAppAuthCertificate -Thumbprint 'ABC123DEF456'
    .EXAMPLE
    Initialize-TkAppAuthCertificate -Subject 'CN=MyAppCert' -AppName 'MyApp'
    .NOTES
    This function requires the user to have appropriate permissions to access the certificate store and create certificates.
#>

function Initialize-TkAppAuthCertificate {
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')]
    param(
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'The thumbprint of the certificate to retrieve. If omitted, a new self-signed certificate is created.'
        )]
        [string]
        $Thumbprint,
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'An optional name to store in the output object (e.g., the associated app name).'
        )]
        [string]
        $AppName,
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'The subject name for the new certificate if no thumbprint is provided.'
        )]
        [string]
        $Subject = 'CN=TkDefaultSelfSignedCert',
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'The certificate store location (e.g., "Cert:\CurrentUser\My").'
        )]
        [string]
        $CertStoreLocation = 'Cert:\CurrentUser\My',
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Exportable key policy for the new certificate.'
        )]
        [ValidateSet('Exportable', 'NonExportable')]
        [string]
        $KeyExportPolicy = 'NonExportable'
    )
    if (-not $script:LogString) {
        Write-AuditLog -Start
    }
    else {
        Write-AuditLog -BeginFunction
    }
    Write-AuditLog '###############################################'
    try {
        if ($Thumbprint) {
            # Retrieve an existing certificate
            $cert = Get-ChildItem -Path $CertStoreLocation | Where-Object { $_.Thumbprint -eq $Thumbprint }
            if (-not $cert) {
                throw "Certificate with thumbprint $Thumbprint not found in $CertStoreLocation."
            }
            Write-AuditLog "Retrieved certificate with thumbprint $Thumbprint from $CertStoreLocation."
        }
        else {
            # Prompt before creating a new certificate
            $shouldProcessTarget = "'Subject:$Subject' in $CertStoreLocation"
            $shouldProcessOperation = 'New-SelfSignedCertificate'
            if ($PSCmdlet.ShouldProcess($shouldProcessTarget, $shouldProcessOperation)) {
                Get-TkExistingCert `
                -CertName $Subject `
                -ErrorAction Stop
                $cert = New-SelfSignedCertificate -Subject $Subject -CertStoreLocation $CertStoreLocation `
                    -KeyExportPolicy $KeyExportPolicy -KeySpec Signature -KeyLength 2048 -KeyAlgorithm RSA -HashAlgorithm SHA256
                Write-AuditLog "Created new self-signed certificate with subject '$Subject' in $CertStoreLocation."
            }
            else {
                Write-AuditLog "Certificate creation was skipped by user confirmation."
                throw 'Certificate creation was skipped by user confirmation.'
            }
        }
        $output = [PSCustomObject]@{
            CertThumbprint = $cert.Thumbprint
            CertExpires    = $cert.NotAfter.ToString('yyyy-MM-dd HH:mm:ss')
        }
        if ($AppName) {
            $output | Add-Member -NotePropertyName 'AppName' -NotePropertyValue $AppName
        }
        return $output
    }
    catch {
        Write-AuditLog -Message "Error occurred: $($_.Exception.Message)" -Severity "Error"
        throw
    }
    finally {
        Write-AuditLog -EndFunction
    }
}
#EndRegion '.\Private\Initialize-TkAppAuthCertificate.ps1' 111
#Region '.\Private\Initialize-TkAppName.ps1' -1

<#
    .SYNOPSIS
    Generates a new application name based on provided prefix, scenario name, and user email.
    .DESCRIPTION
    The Initialize-TkAppName function constructs an application name using a specified prefix, an optional scenario name, and an optional user email. The generated name includes a domain suffix derived from the environment variable USERDNSDOMAIN.
    .PARAMETER Prefix
    A short prefix for your app name (2-4 alphanumeric characters). This parameter is mandatory.
    .PARAMETER ScenarioName
    An optional scenario name to include in the app name (for example, AuditGraphEmail, MemPolicy, etc.). Defaults to "TkEmailApp".
    .PARAMETER UserId
    An optional user email to append an "As-[username]" suffix to the app name. The email must be provided in a valid format.
    .PARAMETER DoNotUseDomainSuffix
    A switch to add a session domain suffix to the app name. If not specified, the domain suffix is derived from the USERDNSDOMAIN environment variable.
    .INPUTS
    System.String
    .OUTPUTS
    System.String
    .EXAMPLE
    PS> Initialize-TkAppName -Prefix "MSN"
    Generates an app name with the prefix "MSN" and default scenario name "TkEmailApp".
    .EXAMPLE
    PS> Initialize-TkAppName -Prefix "MSN" -ScenarioName "AuditGraphEmail"
    Generates an app name with the prefix "MSN" and scenario name "AuditGraphEmail".
    .EXAMPLE
    PS> Initialize-TkAppName -Prefix "MSN" -UserId "helpdesk@mydomain.com"
    Generates an app name with the prefix "MSN" and appends the user suffix derived from the email "helpdesk@mydomain.com".
    .NOTES
    The function logs the process of building the app name and handles errors by logging and throwing them.
#>

function Initialize-TkAppName {
    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(
            Mandatory=$true,
            HelpMessage='A short prefix for your app name (2-4 alphanumeric chars).'
        )]
        [ValidatePattern('^[A-Z0-9]{2,4}$')]
        [string]
        $Prefix,
        [Parameter(
            Mandatory=$false,
            HelpMessage='Optional scenario name (e.g. AuditGraphEmail, MemPolicy, etc.).'
        )]
        [string]
        $ScenarioName = "TkEmailApp",
        [Parameter(
            Mandatory=$false,
            HelpMessage='Optional user email to append "As-[username]" suffix.'
        )]
        [ValidatePattern('^[\w-\.]+@([\w-]+\.)+[\w-]{2,}$')]
        [string]
        $UserId,
        [Parameter(
            Mandatory=$false,
            HelpMessage='Switch to add session domain suffix to the app name.'
        )]
        [switch]
        $DoNotUseDomainSuffix
    )
    begin {
        if (-not $script:LogString) { Write-AuditLog -Start } else { Write-AuditLog -BeginFunction }
    }
    process {
        try {
            Write-AuditLog "Building app name..."
            # Build a user suffix if $UserId is provided
            $userSuffix = ""
            if ($UserId) {
                # e.g. "helpdesk@mydomain.com" -> "As-helpDesk"
                $userPrefix = ($UserId.Split('@')[0])
                $userSuffix = "-As-$userPrefix"
            }
            if ($DoNotUseDomainSuffix) {
                $domainSuffix = "MyDomain"
            }
            else {
                $domainSuffix = $env:USERDNSDOMAIN
            }
            $appName = "GraphToolKit-$Prefix-$ScenarioName"
            Write-AuditLog "Returning app name: $appName (Prefix: $Prefix, Scenario: $ScenarioName, User Suffix: $userSuffix)"
            $appName += "-$domainSuffix"
            $appName += "$userSuffix"
            Write-AuditLog "Returning app name: $appName"
            return $appName
        }
        catch {
            $errorMessage = "An error occurred while building the app name: $_"
            Write-AuditLog -Message $errorMessage -Severity "Error"  # include error severity per StyleGuide
            throw $errorMessage
        }
        finally {
            Write-AuditLog -EndFunction
        }
    }
}
#EndRegion '.\Private\Initialize-TkAppName.ps1' 97
#Region '.\Private\Initialize-TkEmailAppParamsObject.ps1' -1

<#
    .SYNOPSIS
    Initializes a TkEmailAppParams object with the provided parameters.
    .DESCRIPTION
    The Initialize-TkEmailAppParamsObject function creates and returns a new instance of the TkEmailAppParams class using the provided parameters. This function ensures that all necessary parameters are provided and initializes the object accordingly.
    .PARAMETER AppId
    The application ID used to uniquely identify the email application.
    .PARAMETER Id
    The unique identifier for the specific email application instance.
    .PARAMETER AppName
    The name of the email application being initialized.
    .PARAMETER CertificateSubject
    The subject name of the client certificate used by the email application.
    .PARAMETER AppRestrictedSendGroup
    The group that is restricted from sending emails within the application.
    .PARAMETER CertExpires
    The expiration date of the certificate used by the email application.
    .PARAMETER CertThumbprint
    The thumbprint of the certificate used for authentication.
    .PARAMETER ConsentUrl
    The URL where users can provide consent for the email application.
    .PARAMETER DefaultDomain
    The default domain used by the email application for sending emails.
    .PARAMETER SendAsUser
    The user who will send emails on behalf of the email application.
    .PARAMETER SendAsUserEmail
    The email address of the user who will send emails on behalf of the application.
    .PARAMETER TenantID
    The tenant ID associated with the email application.
    .OUTPUTS
    [TkEmailAppParams]
    Returns a new instance of the TkEmailAppParams class initialized with the provided parameters.
    .EXAMPLE
    $tkEmailAppParams = Initialize-TkEmailAppParamsObject -AppId "12345" -Id "67890" -AppName "MyEmailApp" -CertificateSubject "CN=MyCert" -AppRestrictedSendGroup "RestrictedGroup" -CertExpires "2023-12-31" -CertThumbprint "ABCDEF123456" -ConsentUrl "https://consent.url" -DefaultDomain "example.com" -SendAsUser "user@example.com" -SendAsUserEmail "user@example.com" -TenantID "tenant123"
 
    This example initializes a TkEmailAppParams object with the specified parameters.
#>

function Initialize-TkEmailAppParamsObject {
    param (
        [string]$AppId,
        [string]$Id,
        [string]$AppName,
        [string]$CertificateSubject,
        [string]$AppRestrictedSendGroup,
        [string]$CertExpires,
        [string]$CertThumbprint,
        [string]$ConsentUrl,
        [string]$DefaultDomain,
        [string]$SendAsUser,
        [string]$SendAsUserEmail,
        [string]$TenantID
    )
    return [TkEmailAppParams]::new(
        $AppId,
        $Id,
        $AppName,
        $CertificateSubject,
        $AppRestrictedSendGroup,
        $CertExpires,
        $CertThumbprint,
        $ConsentUrl,
        $DefaultDomain,
        $SendAsUser,
        $SendAsUserEmail,
        $TenantID
    )
}
#EndRegion '.\Private\Initialize-TkEmailAppParamsObject.ps1' 68
#Region '.\Private\Initialize-TkM365AuditAppParamsObject.ps1' -1

<#
    .SYNOPSIS
        Initializes a TkM365AuditAppParams object with the provided parameters.
    .DESCRIPTION
        This function initializes a TkM365AuditAppParams object using the parameters provided by the user. It sets up the application name, application ID, object ID, tenant ID, certificate thumbprint, certificate expiration date, consent URL, and various permissions for Microsoft Graph, SharePoint, and Exchange. This allows for the configuration and management of the TkM365AuditAppParams object within the application.
    .PARAMETER AppName
        The name of the application.
    .PARAMETER AppId
        The unique identifier for the application.
    .PARAMETER ObjectId
        The unique identifier for the object.
    .PARAMETER TenantId
        The unique identifier for the tenant.
    .PARAMETER CertThumbprint
        The thumbprint of the certificate used.
    .PARAMETER CertExpires
        The expiration date of the certificate.
    .PARAMETER ConsentUrl
        The URL used for consent.
    .PARAMETER MgGraphPermissions
        An array of permissions for Microsoft Graph.
    .PARAMETER SharePointPermissions
        An array of permissions for SharePoint.
    .PARAMETER ExchangePermissions
        An array of permissions for Exchange.
    .OUTPUTS
        TkM365AuditAppParams
        A new instance of the TkM365AuditAppParams object initialized with the provided parameters.
    .EXAMPLE
        $Params = Initialize-TkM365AuditAppParamsObject -AppName "MyApp" -AppId "12345" -ObjectId "67890" -TenantId "tenant123" -CertThumbprint "ABCDEF" -CertExpires "2023-12-31" -ConsentUrl "https://consent.url" -MgGraphPermissions @("Permission1", "Permission2") -SharePointPermissions @("Permission1") -ExchangePermissions @("Permission1", "Permission2")
#>

function Initialize-TkM365AuditAppParamsObject {
    param (
        [string]$AppName,
        [string]$AppId,
        [string]$ObjectId,
        [string]$TenantId,
        [string]$CertThumbprint,
        [string]$CertExpires,
        [string]$ConsentUrl,
        [string[]]$MgGraphPermissions,
        [string[]]$SharePointPermissions,
        [string[]]$ExchangePermissions
    )
    return [TkM365AuditAppParams]::new(
        $AppName,
        $AppId,
        $ObjectId,
        $TenantId,
        $CertThumbprint,
        $CertExpires,
        $ConsentUrl,
        $MgGraphPermissions,
        $SharePointPermissions,
        $ExchangePermissions
    )
}
#EndRegion '.\Private\Initialize-TkM365AuditAppParamsObject.ps1' 58
#Region '.\Private\Initialize-TkMemPolicyManagerAppParamsObject.ps1' -1

<#
    .SYNOPSIS
    Initializes a TkMemPolicyManagerAppParams object with the provided parameters.
    .DESCRIPTION
    This function creates and returns a new instance of the TkMemPolicyManagerAppParams class using the provided parameters.
    .PARAMETER AppId
    The unique identifier for the application.
    .PARAMETER AppName
    The name of the application to be initialized.
    .PARAMETER CertThumbprint
    The thumbprint of the certificate used for authentication.
    .PARAMETER ObjectId
    The unique identifier for the object.
    .PARAMETER ConsentUrl
    The URL where consent can be granted for the application.
    .PARAMETER PermissionSet
    The set of permissions required by the application.
    .PARAMETER Permissions
    The specific permissions granted to the application.
    .PARAMETER TenantId
    The unique identifier for the tenant.
    .OUTPUTS
    [TkMemPolicyManagerAppParams] The initialized TkMemPolicyManagerAppParams object.
    .EXAMPLE
    $AppParams = Initialize-TkMemPolicyManagerAppParamsObject -AppId "12345" -AppName "MyApp" -CertThumbprint "ABCDEF" -ObjectId "67890" -ConsentUrl "https://consent.url" -PermissionSet "ReadWrite" -Permissions "All" -TenantId "Tenant123"
#>

function Initialize-TkMemPolicyManagerAppParamsObject {
    param (
        [string]$AppId,
        [string]$AppName,
        [string]$CertThumbprint,
        [string]$ObjectId,
        [string]$ConsentUrl,
        [string]$PermissionSet,
        [string]$Permissions,
        [string]$TenantId
    )
    return [TkMemPolicyManagerAppParams]::new(
        $AppId,
        $AppName,
        $CertThumbprint,
        $ObjectId,
        $ConsentUrl,
        $PermissionSet,
        $Permissions,
        $TenantId
    )
}
#EndRegion '.\Private\Initialize-TkMemPolicyManagerAppParamsObject.ps1' 49
#Region '.\Private\Initialize-TkModuleEnv.ps1' -1

<#
    .SYNOPSIS
    Initializes the environment by installing and importing specified PowerShell modules.
    .DESCRIPTION
    The Initialize-TkModuleEnv function installs and imports specified PowerShell modules, either public or pre-release versions, based on the provided parameters. It also ensures that the PowerShellGet module is up-to-date and handles the installation scope, requiring elevation for 'AllUsers' scope. The function logs the installation and import process using Write-AuditLog.
    .PARAMETER PublicModuleNames
    An array of public module names to be installed and imported from the PowerShell Gallery. Each module must exist in the gallery.
    .PARAMETER PublicRequiredVersions
    An array of required versions corresponding to the public module names. Must match the count of PublicModuleNames.
    .PARAMETER PrereleaseModuleNames
    An array of pre-release module names to be installed from the PowerShell Gallery. Used for modules in preview/beta state.
    .PARAMETER PrereleaseRequiredVersions
    An array of required versions corresponding to the pre-release module names. Must match the count of PrereleaseModuleNames.
    .PARAMETER Scope
    The installation scope, either 'AllUsers' (requires elevation) or 'CurrentUser' (default, no elevation needed).
    .PARAMETER ImportModuleNames
    An optional array of module names to be imported after installation. Useful for importing specific modules from a larger package.
    .INPUTS
    None. This function does not accept pipeline input.
    .OUTPUTS
    None. This function does not generate output.
    .EXAMPLE
    $params1 = @{
        PublicModuleNames = "PSnmap","Microsoft.Graph"
        PublicRequiredVersions = "1.3.1","1.23.0"
        ImportModuleNames = "Microsoft.Graph.Authentication", "Microsoft.Graph.Identity.SignIns"
        Scope = "CurrentUser"
    }
    Initialize-TkModuleEnv @params1
    Installs and imports specific modules for Microsoft.Graph.
    .EXAMPLE
    $params2 = @{
        PrereleaseModuleNames = "Sampler", "Pester"
        PrereleaseRequiredVersions = "2.1.5", "4.10.1"
        Scope = "CurrentUser"
    }
    Initialize-TkModuleEnv @params2
    Installs the pre-release versions of Sampler and Pester in the CurrentUser scope.
    .NOTES
    - If Microsoft.Graph is being installed, the function limit is raised to 8192 if it is less than that.
    - The function checks and updates PowerShellGet if needed.
    - The function validates the installation scope and requires elevation for 'AllUsers' scope.
    - The function logs the installation and import process using Write-AuditLog.
#>

function Initialize-TkModuleEnv {
    [CmdletBinding(DefaultParameterSetName = 'Public')]
    param(
        [Parameter(
            ParameterSetName = 'Public',
            Mandatory,
            HelpMessage = 'Array of public module names to be installed from the PowerShell Gallery'
        )]
        [string[]]
        $PublicModuleNames,

        [Parameter(
            ParameterSetName = 'Public',
            Mandatory,
            HelpMessage = 'Array of required versions corresponding to the public module names'
        )]
        [string[]]
        $PublicRequiredVersions,

        [Parameter(
            ParameterSetName = 'Prerelease',
            Mandatory,
            HelpMessage = 'Array of pre-release module names to be installed from the PowerShell Gallery'
        )]
        [string[]]
        $PrereleaseModuleNames,

        [Parameter(
            ParameterSetName = 'Prerelease',
            Mandatory,
            HelpMessage = 'Array of required versions corresponding to the pre-release module names'
        )]
        [string[]]
        $PrereleaseRequiredVersions,

        [Parameter(
            HelpMessage = 'Installation scope, either AllUsers (requires admin) or CurrentUser'
        )]
        [ValidateSet('AllUsers', 'CurrentUser')]
        [string]
        $Scope,

        [Parameter(
            HelpMessage = 'Optional array of module names to import after installation (useful for submodules)'
        )]
        [string[]]
        $ImportModuleNames = $null
    )

    if (-not $script:LogString) {
        Write-AuditLog -Start
    }
    else {
        Write-AuditLog -BeginFunction
    }
    Write-AuditLog '###########################################################'

    try {
        # If Microsoft.Graph is being installed, raise function limit if < 8192.
        if (($PublicModuleNames -match 'Microsoft.Graph') -or ($PrereleaseModuleNames -match 'Microsoft.Graph')) {
            if ($script:MaximumFunctionCount -lt 8192) {
                $script:MaximumFunctionCount = 8192
                Write-AuditLog "Increased maximum function count to $script:MaximumFunctionCount for Microsoft.Graph" -Severity Information
            }
        }

        # Step 1: Check/Update PowerShellGet if needed
        $psGetModules = Get-Module -Name PowerShellGet -ListAvailable
        $hasNonDefaultVer = $false
        foreach ($mod in $psGetModules) {
            if ($mod.Version -ne '1.0.0.1') {
                $hasNonDefaultVer = $true
                break
            }
        }

        if ($hasNonDefaultVer) {
            # Import the latest version
            $latestModule = $psGetModules | Sort-Object Version -Descending | Select-Object -First 1
            Import-Module -Name $latestModule.Name -RequiredVersion $latestModule.Version -ErrorAction Stop
            Write-AuditLog "Imported PowerShellGet version $($latestModule.Version)" -Severity Information
        }
        else {
            if (-not(Test-IsAdmin)) {
                Write-AuditLog 'PowerShellGet is version 1.0.0.1. Please run once as admin to update PowerShellGet.' -Severity Error
                throw 'Elevation required to update PowerShellGet!'
            }
            else {
                Write-AuditLog 'Updating PowerShellGet...' -Severity Information
                [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
                Install-Module PowerShellGet -AllowClobber -Force -ErrorAction Stop
                $psGetModules = Get-Module -Name PowerShellGet -ListAvailable
                $latestModule = $psGetModules | Sort-Object Version -Descending | Select-Object -First 1
                Import-Module -Name $latestModule.Name -RequiredVersion $latestModule.Version -ErrorAction Stop
                Write-AuditLog "Updated and imported PowerShellGet version $($latestModule.Version)" -Severity Information
            }
        }

        # Step 2: Validate scope
        if ($Scope -eq 'AllUsers') {
            if (-not(Test-IsAdmin)) {
                Write-AuditLog "You must be an administrator to install in 'AllUsers' scope." -Severity Error
                throw "Elevation required for 'AllUsers' scope."
            }
            else {
                Write-AuditLog "Installing modules for 'AllUsers' scope." -Severity Information
            }
        }

        # Step 3: Determine module set
        $prerelease = $false
        if ($PSCmdlet.ParameterSetName -eq 'Public') {
            $modules = $PublicModuleNames
            $versions = $PublicRequiredVersions
        }
        elseif ($PSCmdlet.ParameterSetName -eq 'Prerelease') {
            $modules = $PrereleaseModuleNames
            $versions = $PrereleaseRequiredVersions
            $prerelease = $true
        }

        # Step 4: Install/Import each module
        for ($i = 0; $i -lt $modules.Count; $i++) {
            $m = $modules[$i]
            $requiredVersion = $versions[$i] # Using index instead of IndexOf for reliability
            $installed = Get-Module -Name $m -ListAvailable |
                Where-Object { [version]$_.Version -ge [version]$requiredVersion } |
                Sort-Object Version -Descending |
                Select-Object -First 1

            $SelectiveImports = $null
            if ($ImportModuleNames) {
                $SelectiveImports = $ImportModuleNames | Where-Object { $_ -match $m }
            }

            if (-not $installed) {
                $msgPrefix = if ($prerelease) { 'PreRelease' }else { 'stable' }
                Write-AuditLog "The $msgPrefix module $m version $requiredVersion (or higher) is not installed." -Severity Warning
                Write-AuditLog "Installing $m version $requiredVersion -AllowPrerelease:$prerelease."

                try {
                    Install-Module $m -Scope $Scope -RequiredVersion $requiredVersion -AllowPrerelease:$prerelease -ErrorAction Stop
                    Write-AuditLog "$m module successfully installed!" -Severity Information
                }
                catch {
                    Write-AuditLog "Failed to install $m v$requiredVersion`: $(${($_.Exception.Message)})" -Severity Error
                    throw
                }

                if ($SelectiveImports) {
                    foreach ($ModName in $SelectiveImports) {
                        Write-AuditLog "Importing $ModName."
                        try {
                            Import-Module $ModName -ErrorAction Stop
                            Write-AuditLog "Successfully imported $ModName." -Severity Information
                        }
                        catch {
                            Write-AuditLog "Failed to import $ModName`: $($_.Exception.Message)" -Severity Error
                            throw
                        }
                    }
                }
                else {
                    Write-AuditLog "Importing $m"
                    try {
                        Import-Module $m -ErrorAction Stop
                        Write-AuditLog "Successfully imported $m" -Severity Information
                    }
                    catch {
                        Write-AuditLog "Failed to import $m`: $($_.Exception.Message)" -Severity Error
                        throw
                    }
                }
            }
            else {
                Write-AuditLog "$m v$($installed.Version) exists." -Severity Information
                if ($SelectiveImports) {
                    foreach ($ModName in $SelectiveImports) {
                        Write-AuditLog "Importing SubModule: $ModName."
                        try {
                            Import-Module $ModName -ErrorAction Stop
                            Write-AuditLog "Imported SubModule: $ModName." -Severity Information
                        }
                        catch {
                            Write-AuditLog "Failed to import submodule $ModName`: $($_.Exception.Message)" -Severity Error
                            throw
                        }
                    }
                }
                else {
                    Write-AuditLog "Importing $m"
                    try {
                        Import-Module $m -ErrorAction Stop
                        Write-AuditLog "Imported $m" -Severity Information
                    }
                    catch {
                        Write-AuditLog "Failed to import $m`: $($_.Exception.Message)" -Severity Error
                        throw
                    }
                }
            }
        }
    }
    catch {
        Write-AuditLog "Module initialization failed: $($_.Exception.Message)" -Severity Error
        throw
    }
    finally {
        Write-AuditLog -EndFunction
    }
}
#EndRegion '.\Private\Initialize-TkModuleEnv.ps1' 256
#Region '.\Private\Initialize-TkRequiredResourcePermissionObject.ps1' -1

<#
    .SYNOPSIS
    Creates a new required resource permission object for Microsoft Graph and specific scenarios.
    .DESCRIPTION
    The Initialize-TkRequiredResourcePermissionObject function creates a new required resource permission object for Microsoft Graph and specific scenarios. It retrieves service principals by display name, builds an array of MicrosoftGraphRequiredResourceAccess objects, and processes application permissions and scenario-specific permissions.
    .PARAMETER GraphPermissions
    Specifies an array of application (app-only) permissions for Microsoft Graph. Defaults to 'Mail.Send'. This parameter supports multiple permissions.
    .PARAMETER Scenario
    Specifies the scenario for which to include additional permissions. Currently supports '365Audit'.
    .INPUTS
    None
    .OUTPUTS
    [PSCustomObject] containing the RequiredResourceAccessList.
    .EXAMPLE
    PS C:\> Initialize-TkRequiredResourcePermissionObject -GraphPermissions 'User.Read', 'Mail.Send'
 
    Creates a required resource permission object with the specified Graph permissions.
    .EXAMPLE
    PS C:\> Initialize-TkRequiredResourcePermissionObject -Scenario '365Audit'
 
    Creates a required resource permission object for the '365Audit' scenario, including specific SharePoint and Exchange permissions.
    .NOTES
    Author: DougRios | GraphAppToolkit Module
    Last Updated: 2025-03-16
    This function requires the Microsoft.Graph PowerShell module.
#>

function Initialize-TkRequiredResourcePermissionObject {
    [CmdletBinding(DefaultParameterSetName = 'Default')]
    param (
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Specifies an array of application (app-only) permissions for Microsoft Graph. Defaults to ''Mail.Send''. This parameter supports multiple permissions.'
        )]
        [string[]]
        $GraphPermissions = @('Mail.Send'),
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Specifies the scenario for which to include additional permissions. Currently supports ''365Audit''.',
            ParameterSetName = 'Scenario'
        )]
        [ValidateSet('365Audit')]
        [string]
        $Scenario
    )
    process {
        # Start logging
        if (-not $script:LogString) {
            Write-AuditLog -Start
        }
        else {
            Write-AuditLog -BeginFunction
        }
        try {
            Write-AuditLog '###############################################'
            # 1) Retrieve service principals by DisplayName
            Write-AuditLog 'Looking up service principals by display name...'
            $spGraph = Get-MgServicePrincipal -Filter "DisplayName eq 'Microsoft Graph'" -ErrorAction Stop
            # 2) Build an array of [MicrosoftGraphRequiredResourceAccess] objects
            $requiredResourceAccessList = @()
            # Retrieve all application permissions
            $permissionList = Find-MgGraphPermission -PermissionType Application -All
            # region Graph perms
            [Microsoft.Graph.PowerShell.Models.MicrosoftGraphRequiredResourceAccess] $graphRra = $null
            # If GraphPermissions is not null or empty, process them
            if ($GraphPermissions -and $GraphPermissions.Count -gt 0) {
                if (-not $spGraph) {
                    $errorMessage = 'Microsoft Graph Service Principal not found (by display name).'
                    Write-AuditLog -Message $errorMessage -Severity Error
                    throw $errorMessage
                }
                Write-AuditLog "Gathering permissions: $($GraphPermissions -join ', ')"
                $graphRra = [Microsoft.Graph.PowerShell.Models.MicrosoftGraphRequiredResourceAccess]::new()
                $graphRra.ResourceAppId = $spGraph.AppId
                foreach ($permName in $GraphPermissions) {
                    $foundPerm = $permissionList | Where-Object { $_.Name -eq $permName }
                    if ($foundPerm) {
                        # If multiple matches, pick the first
                        $graphRra.ResourceAccess += @{ Id = $foundPerm.Id; Type = 'Role' }
                        Write-AuditLog "Found Graph permission ID for '$permName': $($foundPerm[0].Id)"
                    }
                    else {
                        Write-AuditLog -Severity Warning -Message "Graph Permission '$permName' not found!"
                    }
                }
                if ($graphRra.ResourceAccess) {
                    $requiredResourceAccessList += $graphRra
                }
                else {
                    $errorMessage = "No Graph permissions found for '$($GraphPermissions -join ', ')'. Check the permission names and try again."
                    Write-AuditLog -Message $errorMessage -Severity Error
                    throw $errorMessage
                }
            }
            # endregion
            # region Scenario-specific permissions
            # Scenario 365Audit
            if ($Scenario -eq '365Audit') {
                # region SharePoint perms
                [Microsoft.Graph.PowerShell.Models.MicrosoftGraphRequiredResourceAccess] $spRra = $null
                $spRra = [Microsoft.Graph.PowerShell.Models.MicrosoftGraphRequiredResourceAccess]::new()
                $spRra.ResourceAppId = "00000003-0000-0ff1-ce00-000000000000" # SharePoint Online
                $spRra.ResourceAccess += @{ Id = 'd13f72ca-a275-4b96-b789-48ebcc4da984'; Type = 'Role' }
                $spRra.ResourceAccess += @{ Id = '678536fe-1083-478a-9c59-b99265e6b0d3'; Type = 'Role' }
                $requiredResourceAccessList += $spRra
                # endregion
                # region Exchange perms
                [Microsoft.Graph.PowerShell.Models.MicrosoftGraphRequiredResourceAccess] $exRra = $null
                $exRra = [Microsoft.Graph.PowerShell.Models.MicrosoftGraphRequiredResourceAccess]::new()
                $exRra.ResourceAppId = "00000002-0000-0ff1-ce00-000000000000" # Exchange Online
                $exRra.ResourceAccess += @{ Id = 'dc50a0fb-09a3-484d-be87-e023b12c6440'; Type = 'Role' }
                $requiredResourceAccessList += $exRra
            } # endregion Scenario 365Audit
            # endregion Scenario-specific permissions
            # 3) Build final result object
            $result = [PSCustomObject]@{
                RequiredResourceAccessList = $requiredResourceAccessList
            }
            Write-AuditLog 'Returning context object.'
            return $result
        }
        catch {
            Write-AuditLog -Message "An error occurred: $($_.Exception.Message)" -Severity Error
            throw
        }
        finally {
            Write-AuditLog -EndFunction
        }
    }
}
#EndRegion '.\Private\Initialize-TkRequiredResourcePermissionObject.ps1' 130
#Region '.\Private\New-TkAppRegistration.ps1' -1

<#
    .SYNOPSIS
        Creates a new enterprise app registration in Azure AD.
    .DESCRIPTION
        The New-TkAppRegistration function creates a new enterprise app registration in Azure AD using the provided display name, certificate thumbprint, and additional optional parameters such as required resource access list, sign-in audience, certificate store location, and descriptive notes about this app's purpose or usage.
    .PARAMETER DisplayName
        The display name for the new app registration, which must be clearly defined and descriptive.
    .PARAMETER RequiredResourceAccessList
        An array of MicrosoftGraphRequiredResourceAccess objects to configure multi-resource access modes securely.
    .PARAMETER SignInAudience
        The sign-in audience for the app registration. Valid values include 'AzureADMyOrg', 'AzureADMultipleOrgs', and 'AzureADandPersonalMicrosoftAccount'.
    .PARAMETER CertThumbprint
        The thumbprint of the certificate used to secure this app registration, ensuring the certificate is valid and present.
    .PARAMETER CertStoreLocation
        The certificate store location, for example "Cert:\CurrentUser\My", where the certificate is located.
    .PARAMETER Notes
        A descriptive note about this app's purpose or usage to provide context and clarity.
    .INPUTS
        None.
    .OUTPUTS
        [Microsoft.Graph.PowerShell.Models.MicrosoftGraphApplication1] representing the newly created app registration.
    .EXAMPLE
        $AppRegistration = New-TkAppRegistration -DisplayName "MyApp" -CertThumbprint "ABC123" -Notes "This is a sample app registration for enterprise use."
    .NOTES
    This function requires the Microsoft.Graph PowerShell module.
    Required permissions:
    - Application.ReadWrite.All
    - Directory.ReadWrite.All
#>

function New-TkAppRegistration {
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')]
    [OutputType([Microsoft.Graph.PowerShell.Models.MicrosoftGraphApplication1])]
    param (
        [Parameter(
            Mandatory = $true,
            HelpMessage = `
                'The display name for the new app registration.'
        )]
        [string]
        $DisplayName,
        [Parameter(
            Mandatory = $false,
            HelpMessage = `
                'An array of MicrosoftGraphRequiredResourceAccess objects to configure multi-resource access modes securely.'
        )]
        [Microsoft.Graph.PowerShell.Models.MicrosoftGraphRequiredResourceAccess[]]
        $RequiredResourceAccessList,
        [Parameter(
            HelpMessage = `
                'The sign-in audience for the app registration. Valid values include ''AzureADMyOrg'', ''AzureADMultipleOrgs'', and ''AzureADandPersonalMicrosoftAccount''.'
        )]
        [ValidateSet('AzureADMyOrg', 'AzureADMultipleOrgs', 'AzureADandPersonalMicrosoftAccount')]
        [string]
        $SignInAudience = 'AzureADMyOrg',
        [Parameter(
            Mandatory = $true,
            HelpMessage = `
                'The thumbprint of the certificate used to secure this app registration, ensuring the certificate is valid and present.'
        )]
        [string]
        $CertThumbprint,
        [Parameter(
            Mandatory = $false,
            HelpMessage = `
                'The certificate store location (e.g., "Cert:\CurrentUser\My") where the certificate is located.'
        )]
        [string]
        $CertStoreLocation = 'Cert:\CurrentUser\My',
        [Parameter(
            Mandatory = $false,
            HelpMessage = `
                'A descriptive note about this app''s purpose or usage to provide context and clarity.'
        )]
        [string]
        $Notes
    )
    # Begin Logging
    if (-not $script:LogString) {
        Write-AuditLog -Start
    }
    else {
        Write-AuditLog -BeginFunction
    }
    Write-AuditLog '###############################################'
    try {
        Write-AuditLog "Creating new enterprise app registration for '$DisplayName'."
        if ($CertThumbprint) {
            # 1) Retrieve the certificate from the CurrentUser store
            $cert = Get-ChildItem -Path $CertStoreLocation | Where-Object { $_.Thumbprint -eq $CertThumbprint }
            if (-not $cert) {
                throw "Certificate with thumbprint $CertThumbprint not found in $CertStoreLocation."
            }
            $shouldProcessTarget = "'$DisplayName' for sign-in audience '$SignInAudience' with certificate thumbprint $CertThumbprint."
            $shouldProcessOperation = 'New-MgApplication'
            if ($PSCmdlet.ShouldProcess($shouldProcessTarget, $shouldProcessOperation)) {
                $MgApplicationParams = @{
                    DisplayName            = $DisplayName
                    Notes                  = $Notes
                    SignInAudience         = $SignInAudience
                    RequiredResourceAccess = $RequiredResourceAccessList
                    AdditionalProperties   = @{}
                    KeyCredentials         = @(
                        @{
                            Type  = 'AsymmetricX509Cert'
                            Usage = 'Verify'
                            Key   = $Cert.RawData
                        }
                    )
                    Web                    = @{ RedirectUris = @('https://login.microsoftonline.com/common/oauth2/nativeclient') }
                }
                $appRegistration = New-MgApplication @MgApplicationParams
            }
            if (-not $appRegistration) {
                throw "The app creation failed for '$DisplayName'."
            }
            Write-AuditLog "App registration created with app Object ID $($appRegistration.Id)."
            return $appRegistration
        }
        else {
            throw 'CertThumbprint is required to create an app registration. No other methods are supported yet.'
        }
    }
    catch {
        throw
    }
    finally {
        Write-AuditLog -EndFunction
    }
}
#EndRegion '.\Private\New-TkAppRegistration.ps1' 130
#Region '.\Private\New-TkAppSpOauth2Registration.ps1' -1

<#
    .SYNOPSIS
    Initializes the Service Principal registration for a given application.
    .DESCRIPTION
    This function sets up the Service Principal registration for an application in Azure AD. It supports certificate-based authentication and grants OAuth2 permissions to the Service Principal.
    .PARAMETER AppRegistration
    The App Registration object containing various properties.
    .PARAMETER RequiredResourceAccessList
    The list of required resource access for the Service Principal.
    .PARAMETER Context
    The Microsoft Graph context that we are currently in.
    .PARAMETER Scopes
    One or more OAuth2 scopes to grant. Defaults to Mail.Send.
    .PARAMETER AuthMethod
    Authentication method to use. Valid values are 'Certificate', 'ClientSecret', 'ManagedIdentity', 'None'.
    .PARAMETER CertThumbprint
    Certificate thumbprint if using Certificate-based authentication.
    .PARAMETER CertStoreLocation
    The certificate store location (e.g., "Cert:\CurrentUser\My"). Defaults to 'Cert:\CurrentUser\My'.
    .EXAMPLE
    $appRegistration = Get-MgApplication -AppId "your-app-id"
    $requiredResourceAccessList = @()
    $context = [PSCustomObject]@{ TenantId = "your-tenant-id" }
    New-TkAppSpOauth2Registration -AppRegistration $appRegistration -RequiredResourceAccessList $requiredResourceAccessList -Context $context
    .NOTES
    This function requires the Microsoft.Graph PowerShell module.
#>


function New-TkAppSpOauth2Registration {
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')]
    param(
        [Parameter(
            Mandatory = $true,
            HelpMessage = 'The App Registration object containing various properties.'
        )]
        [Microsoft.Graph.PowerShell.Models.IMicrosoftGraphApplication]
        $AppRegistration,
        [Parameter(
            Mandatory = $true,
            HelpMessage = 'The list of required resource access for the Service Principal.'
        )]
        [PSCustomObject[]]
        $RequiredResourceAccessList,
        [Parameter(
            Mandatory = $true,
            HelpMessage = 'The Microsoft Graph context that we are currently in.'
        )]
        [PSCustomObject]
        $Context,
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'One or more OAuth2 scopes to grant. Defaults to Mail.Send.'
        )]
        [psobject[]]
        $Scopes = [PSCustomObject]@{
            Graph = @('Mail.Send')
        },
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Authentication method to use. Valid values are "Certificate", "ClientSecret", "ManagedIdentity", "None".'
        )]
        [ValidateSet('Certificate', 'ClientSecret', 'ManagedIdentity', 'None')]
        [string]
        $AuthMethod = 'Certificate',
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Certificate thumbprint if using Certificate-based authentication.'
        )]
        [string]
        $CertThumbprint,
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'The certificate store location (e.g., "Cert:\CurrentUser\My"). Defaults to "Cert:\CurrentUser\My".'
        )]
        [string]
        $CertStoreLocation = 'Cert:\CurrentUser\My'
    )
    begin {
        if (-not $script:LogString) {
            Write-AuditLog -Start
        }
        else {
            Write-AuditLog -BeginFunction
        }
        Write-AuditLog '###############################################'
        if ($AuthMethod -eq 'Certificate' -and -not $CertThumbprint) {
            throw "CertThumbprint is required when AuthMethod is 'Certificate'."
        }

        $cert = $null
    }
    process {
        try {
            # 1. If using certificate auth, retrieve the certificate
            if ($AuthMethod -eq 'Certificate') {
                Write-AuditLog "Retrieving certificate with thumbprint $CertThumbprint."
                $cert = Get-ChildItem -Path $CertStoreLocation | Where-Object { $_.Thumbprint -eq $CertThumbprint }
                if (-not $cert) {
                    throw "Certificate with thumbprint $CertThumbprint not found in $CertStoreLocation."
                }
            }
            $shouldProcessTarget = "'$($AppRegistration.DisplayName)' for tenant $($Context.TenantId)."
            $shouldProcessOperation = 'New-MgServicePrincipal'
            if ($PSCmdlet.ShouldProcess($shouldProcessTarget, $shouldProcessOperation)) {
                # 2. Create a Service Principal for the app (if not existing).
                Write-AuditLog "Creating service principal for app with AppId $($AppRegistration.AppId)."
                [void](New-MgServicePrincipal -AppId $AppRegistration.AppId -AdditionalProperties @{})
            }
            # 3. Get the client Service Principal for the created app.
            $clientSp = Get-MgServicePrincipal -Filter "appId eq '$($AppRegistration.AppId)'"
            if (-not $clientSp) {
                Write-AuditLog "Client service principal not found for $($AppRegistration.AppId)." -Severity Error
                throw 'Unable to find client service principal.'
            }
            $shouldProcessTarget = "'$($clientSp.DisplayName)' requested scopes for tenant $($Context.TenantId)."
            $shouldProcessOperation = 'New-MgOauth2PermissionGrant'
            if ($PSCmdlet.ShouldProcess($shouldProcessTarget, $shouldProcessOperation)) {
                $i = 0
                foreach ($resource in $RequiredResourceAccessList) {
                    # 4. Combine all scopes into a single space-delimited string
                    switch ($i) {
                        0 {
                            $scopesList = $Scopes.Graph
                            $resourceId = (Get-MgServicePrincipal -Filter "DisplayName eq 'Microsoft Graph'").Id
                        }
                        1 {
                            $scopesList = $Scopes.SharePoint
                            $resourceId = (Get-MgServicePrincipal -Filter "DisplayName eq 'Office 365 SharePoint Online'").Id
                        }
                        2 {
                            $scopesList = $Scopes.Exchange
                            $resourceId = (Get-MgServicePrincipal -Filter "DisplayName eq 'Office 365 Exchange Online'").Id
                        }
                        ($i > 2) { throw 'Too many resources in RequiredResourceAccessList.' }
                        Default { Write-AuditLog "No scopes found for $resource." }
                    }
                    $combinedScopes = $scopesList -join ' '
                    # Foreach resource id start
                    Write-AuditLog "Granting the following scope(s) to Service Principal for: $($clientSp.DisplayName): $combinedScopes"
                    $mgOauth2PermissionGrantParams = @{
                        ClientId    = $clientSp.Id
                        ConsentType = 'AllPrincipals'
                        ResourceId  = $resourceId
                        Scope       = $combinedScopes
                    }
                    [void](New-MgOauth2PermissionGrant -BodyParameter $mgOauth2PermissionGrantParams -Confirm:$false -ErrorAction Stop)
                    Write-AuditLog "Admin consent granted for $resourceId with scopes: $combinedScopes."
                    Start-Sleep -Seconds 2
                    $i++
                }
            }
            $redirectUri = "&redirect_uri=https://login.microsoftonline.com/common/oauth2/nativeclient"
            # 5. Build the admin consent URL
            $adminConsentUrl = `
                'https://login.microsoftonline.com/' `
                + $Context.TenantId `
                + '/adminconsent?client_id=' `
                + $AppRegistration.AppId `
                + $redirectUri
            Write-Verbose 'Please go to the following URL in your browser to provide admin consent:' -Verbose
            Write-AuditLog "`n`n$adminConsentUrl`n" -Severity information -InformationAction Continue
            # For each end
            Write-Verbose 'After providing admin consent, you can use the following command for certificate-based auth:' -Verbose
            if ($AuthMethod -eq 'Certificate') {
                $connectGraph = 'Connect-MgGraph -ClientId "' + $AppRegistration.AppId + '" -TenantId "' +
                $Context.TenantId + '" -CertificateName "' + $cert.SubjectName.Name + '"'
                Write-AuditLog "`n`n$connectGraph`n" -Severity Information -InformationAction Continue
            }
            else {
                # Placeholder for other auth methods
                Write-AuditLog "Future logic for $AuthMethod auth can go here."
                throw "AuthMethod $AuthMethod is not yet implemented."
            }
            return $adminConsentUrl
        }
        catch {
            Write-AuditLog -Message "Error occurred: $($_.Exception.Message)" -Severity "Error"
            throw
        }
    }
    end {
        Write-AuditLog -EndFunction
    }
}
#EndRegion '.\Private\New-TkAppSpOauth2Registration.ps1' 185
#Region '.\Private\New-TkExchangeEmailAppPolicy.ps1' -1

<#
    .SYNOPSIS
    Creates a new Exchange email application policy and optionally adds a user to a mail-enabled sending group.
    .DESCRIPTION
    The New-TkExchangeEmailAppPolicy function creates a new Exchange application access policy for a specified application registration and mail-enabled sending group. Optionally, it can add an authorized sender to the mail-enabled sending group.
    .PARAMETER AppRegistration
    The application registration object. This parameter is mandatory.
    .PARAMETER MailEnabledSendingGroup
    The mail-enabled sending group. This parameter is mandatory.
    .PARAMETER AuthorizedSenderUserName
    The username of the authorized sender to be added to the mail-enabled sending group. This parameter is optional.
    .EXAMPLE
    $AppRegistration = Get-MgApplication -ApplicationId "your-app-id"
    New-TkExchangeEmailAppPolicy -AppRegistration $AppRegistration -MailEnabledSendingGroup "YourGroup" -AuthorizedSenderUserName "UserName"
    This example creates a new Exchange application access policy for the specified application registration and mail-enabled sending group, and adds the specified user to the mail-enabled sending group.
    .EXAMPLE
    $AppRegistration = Get-MgApplication -ApplicationId "your-app-id"
    New-TkExchangeEmailAppPolicy -AppRegistration $AppRegistration -MailEnabledSendingGroup "YourGroup"
 
    This example creates a new Exchange application access policy for the specified application registration and mail-enabled sending group without adding any user to the group.
    .NOTES
    This function uses the Microsoft Graph PowerShell module and requires appropriate permissions to manage Exchange policies and distribution groups.
#>

function New-TkExchangeEmailAppPolicy {
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')]
    param (
        [Parameter(
            Mandatory = $true,
            HelpMessage = 'The application registration object. This parameter is mandatory.'
        )]
        [Microsoft.Graph.PowerShell.Models.IMicrosoftGraphApplication]
        $AppRegistration,
        [Parameter(
            Mandatory = $true,
            HelpMessage = 'The mail-enabled sending group. This parameter is mandatory.'
        )]
        [string]
        $MailEnabledSendingGroup,
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'The username of the authorized sender to be added to the mail-enabled sending group. This parameter is optional.'
        )]
        [string]
        $AuthorizedSenderUserName
    )
    # Begin Logging
    if (!($script:LogString)) {
        Write-AuditLog -Start
    }
    else {
        Write-AuditLog -BeginFunction
    }
    try {
        $shouldProcessTarget = "'$MailEnabledSendingGroup, $AuthorizedSenderUserName' on AppId '$($AppRegistration.AppId)'"
        $shouldProcessOperation = 'Add-DistributionGroupMember, New-ApplicationAccessPolicy'
        if ($PSCmdlet.ShouldProcess($shouldProcessTarget, $shouldProcessOperation)) {
            if ($PSBoundParameters.ContainsKey('AuthorizedSenderUserName')) {
                Write-AuditLog "Adding User: $AuthorizedSenderUserName to the Mail Enabled Sending Group: $MailEnabledSendingGroup"
                Add-DistributionGroupMember `
                    -Identity $MailEnabledSendingGroup `
                    -Member $AuthorizedSenderUserName `
                    -Confirm:$false `
                    -ErrorAction Stop
            }
            Write-AuditLog -Message "Creating Exchange Application policy for $($MailEnabledSendingGroup) for AppId $($AppRegistration.AppId)."
            New-ApplicationAccessPolicy -AppId $AppRegistration.AppId `
                -PolicyScopeGroupId $MailEnabledSendingGroup -AccessRight RestrictAccess `
                -Description 'Limit MSG application to only send emails as a group of users' `
                -Confirm:$false `
                -ErrorAction Stop | Out-Null
            Write-AuditLog -Message "Created Exchange Application policy for $($MailEnabledSendingGroup)."
        }
    }
    catch {
        Write-AuditLog -Message "Error creating Exchange Application policy: $_" -Severity "Error"
        throw
    }
    Write-AuditLog -EndFunction
}
#EndRegion '.\Private\New-TkExchangeEmailAppPolicy.ps1' 80
#Region '.\Private\Set-TkJsonSecret.ps1' -1

<#
    .SYNOPSIS
    Stores a JSON representation of an object as a secret in a specified vault.
    .DESCRIPTION
    The Set-TkJsonSecret function converts a given object to JSON format and stores it as a secret in a specified vault.
    If the vault is not registered, it will be auto-registered using the specified vault module.
    The function supports overwriting existing secrets if the -Overwrite switch is specified.
    .PARAMETER Name
    The name under which to store the secret. This parameter is mandatory.
    .PARAMETER InputObject
    The object to convert to JSON and store. This parameter is mandatory.
    .PARAMETER VaultName
    The name of the vault where the secret will be stored. Defaults to 'GraphEmailAppLocalStore'.
    .PARAMETER VaultModuleName
    The name of the vault module to use if auto-registering the vault. Defaults to 'SecretManagement.JustinGrote.CredMan'.
    .PARAMETER Overwrite
    Switch to overwrite an existing secret of the same name without prompting.
    .EXAMPLE
    Set-TkJsonSecret -Name 'MySecret' -InputObject $myObject
    This example converts the object stored in $myObject to JSON and stores it as a secret named 'MySecret' in the default vault.
    .EXAMPLE
    Set-TkJsonSecret -Name 'MySecret' -InputObject $myObject -VaultName 'MyCustomVault' -VaultModuleName 'MyVaultModule' -Overwrite
    This example converts the object stored in $myObject to JSON and stores it as a secret named 'MySecret' in the 'MyCustomVault' vault, using 'MyVaultModule' for auto-registration if needed, and overwrites any existing secret with the same name.
    .NOTES
    If the specified vault is not registered, it will be auto-registered using the specified vault module.
    If the secret already exists and the -Overwrite switch is not specified, an error will be thrown.
#>

function Set-TkJsonSecret {
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')]
    [OutputType([string])]
    param(
        [Parameter(
            Mandatory = $true,
            HelpMessage = 'The name under which to store the secret. Must be a non-empty string.'
        )]
        [ValidateNotNullOrEmpty()]
        [string]
        $Name,
        [Parameter(
            Mandatory = $true,
            HelpMessage = 'The object to convert to JSON and store. Must be a valid PSObject.'
        )]
        [ValidateNotNullOrEmpty()]
        [PSObject]
        $InputObject,
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Name of the vault. Defaults to GraphEmailAppLocalStore.'
        )]
        [string]
        $VaultName = 'GraphEmailAppLocalStore',
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Name of the vault module to use if auto-registering. Defaults to SecretManagement.JustinGrote.CredMan.'
        )]
        [string]
        $VaultModuleName = 'SecretManagement.JustinGrote.CredMan',
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Overwrite existing secret of the same name without prompting.'
        )]
        [switch]
        $Overwrite
    )
    if (!($script:logString)) { Write-AuditLog -Start } else { Write-AuditLog -BeginFunction }
    try {
        Write-AuditLog '###############################################'
        # Auto-register vault if missing
        if (!(Get-SecretVault -Name $VaultName -ErrorAction SilentlyContinue)) {
            Write-AuditLog -Message "Registering $VaultName using $VaultModuleName"
            Register-SecretVault -Name $VaultName -ModuleName $VaultModuleName -ErrorAction Stop
            Write-AuditLog -Message "Vault '$VaultName' registered."
        }
        else {
            Write-AuditLog "Vault '$VaultName' is already registered."
        }
        # Check if secret already exists
        $secretExists = (Get-SecretInfo -Name $Name -Vault $VaultName -ErrorAction SilentlyContinue)
        if ($secretExists) {
            if ($Overwrite) {
                $shouldProcessOperation = 'Remove-Secret'
                $shouldProcessTarget = "Name: '$Name' in vault '$VaultName'."
                if ($PSCmdlet.ShouldProcess($shouldProcessTarget, $shouldProcessOperation)) {
                    Write-AuditLog -Message "Overwriting existing secret '$Name' in vault '$VaultName'."
                    Remove-Secret -Name $Name -Vault $VaultName -Confirm:$false -ErrorAction Stop
                }
                else {
                    Write-AuditLog -Message "Overwrite of existing secret '$Name' in vault '$VaultName' was cancelled." -Severity Warning
                    throw
                }
            }
            else {
                Write-AuditLog -Message "Secret '$Name' already exists. Remove it or specify -Overwrite to overwrite." -Verbose
                throw
            }
        }
        $json = ($InputObject | ConvertTo-Json -Compress)
        Set-Secret -Name $Name -Secret $json -Vault $VaultName -ErrorAction Stop
        Write-AuditLog -Message "Secret '$Name' saved to vault '$VaultName'."
        Write-AuditLog -EndFunction
        return $Name
    }
    catch {
        throw
    }
}
#EndRegion '.\Private\Set-TkJsonSecret.ps1' 107
#Region '.\Private\Test-IsAdmin.ps1' -1

function Test-IsAdmin {
    <#
    .SYNOPSIS
    Checks if the current user is an administrator on the machine.
    .DESCRIPTION
    This private function returns a Boolean value indicating whether
    the current user has administrator privileges on the machine.
    It does this by creating a new WindowsPrincipal object, passing
    in a WindowsIdentity object representing the current user, and
    then checking if that principal is in the Administrator role.
    .INPUTS
    None.
    .OUTPUTS
    Boolean. Returns True if the current user is an administrator, and False otherwise.
    .EXAMPLE
    PS C:\> Test-IsAdmin
    True
    #>

    # Create a new WindowsPrincipal object for the current user and check if it is in the Administrator role
    (New-Object Security.Principal.WindowsPrincipal ([Security.Principal.WindowsIdentity]::GetCurrent())).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)
}
#EndRegion '.\Private\Test-IsAdmin.ps1' 22
#Region '.\Private\Write-AuditLog.ps1' -1

<#
    .SYNOPSIS
        Writes log messages to the console and updates the script-wide log variable.
    .DESCRIPTION
        The Write-AuditLog function writes log messages to the console based on the severity (Verbose, Warning, or Error) and updates
        the script-wide log variable ($script:LogString) with the log entry. You can use the Start, End, and EndFunction switches to
        manage the lifecycle of the logging.
    .INPUTS
        System.String
        You can pipe a string to the Write-AuditLog function as the Message parameter.
        You can also pipe an object with a Severity property as the Severity parameter.
    .OUTPUTS
        None
        The Write-AuditLog function doesn't output any objects to the pipeline. It writes messages to the console and updates the
        script-wide log variable ($script:LogString).
    .PARAMETER BeginFunction
        Sets the message to "Begin [FunctionName] function log.", where FunctionName is the name of the calling function, and adds it to the log variable.
    .PARAMETER Message
        The message string to log.
    .PARAMETER Severity
        The severity of the log message. Accepted values are 'Information', 'Warning', 'Error'. Defaults to 'Verbose'.
    .PARAMETER Start
        Initializes the script-wide log variable and sets the message to "Begin [FunctionName] Log.", where FunctionName is the name of the calling function.
    .PARAMETER End
        Sets the message to "End Log" and exports the log to a CSV file if the OutputPath parameter is provided.
    .PARAMETER EndFunction
        Sets the message to "End [FunctionName] log.", where FunctionName is the name of the calling function, and adds it to the log variable.
    .PARAMETER OutputPath
        The file path for exporting the log to a CSV file when using the End switch.
    .EXAMPLE
        Write-AuditLog -Message "This is a test message."
 
        Writes a test message with the default severity (Verbose) to the console and adds it to the log variable.
    .EXAMPLE
        Write-AuditLog -Message "This is a warning message." -Severity "Warning"
 
        Writes a warning message to the console and adds it to the log variable.
    .EXAMPLE
        Write-AuditLog -Start
 
        Initializes the log variable and sets the message to "Begin [FunctionName] Log.", where FunctionName is the name of the calling function.
    .EXAMPLE
        Write-AuditLog -BeginFunction
 
        Sets the message to "Begin [FunctionName] function log.", where FunctionName is the name of the calling function, and adds it to the log variable.
    .EXAMPLE
        Write-AuditLog -EndFunction
 
        Sets the message to "End [FunctionName] log.", where FunctionName is the name of the calling function, and adds it to the log variable.
    .EXAMPLE
        Write-AuditLog -End -OutputPath "C:\Logs\auditLog.csv"
 
        Sets the message to "End Log", adds it to the log variable, and exports the log to a CSV file.
    .NOTES
    Author: DrIOSx
#>

function Write-AuditLog {
    [CmdletBinding(
        DefaultParameterSetName = 'Default'
    )]
    param(
        ###
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Input a Message string.',
            Position = 0,
            ParameterSetName = 'Default',
            ValueFromPipeline = $true
        )]
        [ValidateNotNullOrEmpty()]
        [string]$Message,
        ###
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Information, Warning or Error.',
            Position = 1,
            ParameterSetName = 'Default',
            ValueFromPipelineByPropertyName = $true
        )]
        [ValidateNotNullOrEmpty()]
        [ValidateSet('Information', 'Warning', 'Error', 'Verbose')]
        [string]$Severity = 'Verbose',
        ###
        [Parameter(
            Mandatory = $false,
            ParameterSetName = 'End'
        )]
        [switch]$End,
        ###
        [Parameter(
            Mandatory = $false,
            ParameterSetName = 'BeginFunction'
        )]
        [switch]$BeginFunction,
        ###
        [Parameter(
            Mandatory = $false,
            ParameterSetName = 'EndFunction'
        )]
        [switch]$EndFunction,
        ###
        [Parameter(
            Mandatory = $false,
            ParameterSetName = 'Start'
        )]
        [switch]$Start,
        ###
        [Parameter(
            Mandatory = $false,
            ParameterSetName = 'End'
        )]
        [string]$OutputPath
    )
    begin {
        $ErrorActionPreference = 'SilentlyContinue'
        # Define variables to hold information about the command that was invoked.
        $moduleName = $Script:MyInvocation.MyCommand.Name -replace '\..*'
        $callStack = Get-PSCallStack
        if ($callStack.Count -gt 1) {
            $funcName = $callStack[1].Command
        }
        else {
            $funcName = 'DirectCall'  # Or any other default name you prefer
        }
        $moduleVer = $MyInvocation.MyCommand.Version.ToString()
        # Set the error action preference to continue.
        $ErrorActionPreference = 'Continue'
    }
    process {
        try {
            if (-not $Start -and -not (Test-Path variable:script:LogString)) {
                throw "The logging variable is not initialized. Please call Write-AuditLog with the -Start switch or ensure $script:LogString is set."
            }
            $function = $($funcName + '.v' + $moduleVer)
            if ($Start) {
                $script:LogString = @()
                $Message = '+++ Begin Log +++ | ' + $function + ' |'
            }
            elseif ($BeginFunction) {
                $Message = '>>> Begin Function Log >>> | ' + $function + ' |'
            }
            $logEntry = [pscustomobject]@{
                Time      = ((Get-Date).ToString('yyyy-MM-dd hh:mmTss'))
                Module    = $moduleName
                PSVersion = ($PSVersionTable.PSVersion).ToString()
                PSEdition = ($PSVersionTable.PSEdition).ToString()
                IsAdmin   = $(Test-IsAdmin)
                User      = "$Env:USERDOMAIN\$Env:USERNAME"
                HostName  = $Env:COMPUTERNAME
                InvokedBy = $function
                Severity  = $Severity
                Message   = $Message
                RunID     = -1
            }
            if ($BeginFunction) {
                $maxRunID = ($script:LogString | Where-Object { $_.InvokedBy -eq $function } | Measure-Object -Property RunID -Maximum).Maximum
                if ($null -eq $maxRunID) { $maxRunID = -1 }
                $logEntry.RunID = $maxRunID + 1
            }
            else {
                $lastRunID = ($script:LogString | Where-Object { $_.InvokedBy -eq $function } | Select-Object -Last 1).RunID
                if ($null -eq $lastRunID) { $lastRunID = 0 }
                $logEntry.RunID = $lastRunID
            }
            if ($EndFunction) {
                $functionStart = "$((($script:LogString | Where-Object {$_.InvokedBy -eq $function -and $_.RunId -eq $lastRunID } | Sort-Object Time)[0]).Time)"
                $startTime = ([DateTime]::ParseExact("$functionStart", 'yyyy-MM-dd hh:mmTss', $null))
                $endTime = Get-Date
                $timeTaken = $endTime - $startTime
                $Message = '<<< End Function Log <<< | ' + $function + ' | Runtime: ' + "$($timeTaken.Minutes) min $($timeTaken.Seconds) sec"
                $logEntry.Message = $Message
            }
            elseif ($End) {
                $startTime = ([DateTime]::ParseExact($($script:LogString[0].Time), 'yyyy-MM-dd hh:mmTss', $null))
                $endTime = Get-Date
                $timeTaken = $endTime - $startTime
                $Message = '--- End Log | ' + $function + ' | Runtime: ' + "$($timeTaken.Minutes) min $($timeTaken.Seconds) sec"
                $logEntry.Message = $Message
            }
            $script:LogString += $logEntry
            switch ($Severity) {
                'Warning' {
                    Write-Warning ('[WARNING] ! ' + $Message)
                }
                'Error' { Write-Error ('[ERROR] X - ' + "[$((Get-Date).ToString('yyyy.MM.dd HH:mm:ss.fff'))] " + $funcName + ' ' + $Message) -ErrorAction Continue }
                'Verbose' { Write-Verbose ('~ ' + "[$((Get-Date).ToString('yyyy.MM.dd HH:mm:ss.fff'))] " + $Message) }
                Default { Write-Information ("[NFO] [$((Get-Date).ToString('yyyy.MM.dd HH:mm:ss.fff'))] " + $Message) }
            }
        }
        catch {
            throw
        }
    }
    end {
        try {
            if ($End) {
                if (-not [string]::IsNullOrEmpty($OutputPath)) {
                    $script:LogString | Export-Csv -Path $OutputPath -NoTypeInformation
                    Write-Verbose "LogPath: $(Split-Path -Path $OutputPath -Parent)"
                }
                else {
                    throw 'OutputPath is not specified for End action.'
                }
            }
        }
        catch {
            throw "Error in Write-AuditLog (end block): $($_.Exception.Message)"
        }
    }
}
#EndRegion '.\Private\Write-AuditLog.ps1' 211
#Region '.\Public\New-MailEnabledSendingGroup.ps1' -1

<#
    .SYNOPSIS
        Creates or retrieves a mail-enabled security group with a custom or default domain.
    .DESCRIPTION
        The New-MailEnabledSendingGroup function ensures that a mail-enabled security group is
        available for restricting email sending in Exchange Online. If a group of the specified
        name already exists and is security-enabled, the function returns that group. Otherwise,
        it creates a new security-enabled distribution group. You can specify either a custom
        primary SMTP address (via the 'CustomDomain' parameter set) or construct one using an
        alias and default domain (via the 'DefaultDomain' parameter set).
        By default, the 'CustomDomain' parameter set is used. If you wish to construct the SMTP
        address from the alias, switch to the 'DefaultDomain' parameter set.
    .PARAMETER Name
        The name of the mail-enabled security group to create or retrieve. This is also used as
        the alias if no separate Alias parameter is provided.
    .PARAMETER Alias
        An optional alias for the group. If omitted, the group name is used as the alias.
    .PARAMETER PrimarySmtpAddress
        (CustomDomain parameter set) The full SMTP address for the group (e.g. "MyGroup@contoso.com").
        This parameter is mandatory when using the 'CustomDomain' parameter set.
    .PARAMETER DefaultDomain
        (DefaultDomain parameter set) The domain portion to be appended to the group alias (e.g.
        "Alias@DefaultDomain"). This parameter is mandatory when using the 'DefaultDomain' parameter set.
    .PARAMETER LogOutputPath
        An optional path to output the log file. If not provided, logs will not be written to a file.
    .EXAMPLE
        PS C:\> New-MailEnabledSendingGroup -Name "SecureSenders" -DefaultDomain "contoso.com"
        Creates a new mail-enabled security group named "SecureSenders" with a primary SMTP address
        of SecureSenders@contoso.com.
    .EXAMPLE
        PS C:\> New-MailEnabledSendingGroup -Name "SecureSenders" -Alias "Senders" -PrimarySmtpAddress "Senders@customdomain.org"
        Creates a new mail-enabled security group named "SecureSenders" with an alias "Senders"
        and a primary SMTP address of Senders@customdomain.org.
    .INPUTS
        None. This function does not accept pipeline input.
    .OUTPUTS
        Microsoft.Exchange.Data.Directory.Management.DistributionGroup
        Returns the newly created or existing mail-enabled security group object.
    .NOTES
        - Requires connectivity to Exchange Online (Connect-TkMsService -ExchangeOnline).
        - The caller must have sufficient privileges to create or modify distribution groups.
        - DefaultParameterSetName = 'CustomDomain'.
#>

function New-MailEnabledSendingGroup {
    [CmdletBinding(SupportsShouldProcess = $true , DefaultParameterSetName = 'CustomDomain')]
    param (
        [Parameter(
            Mandatory = $true,
            HelpMessage = 'Specifies the name of the mail enabled sending group.'
        )]
        [string]
        $Name,
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Optional alias for the group. If not provided, the group name will be used.'
        )]
        [string]
        $Alias,
        [Parameter(
            Mandatory = $true,
            ParameterSetName = 'CustomDomain',
            HelpMessage = 'Specifies the primary SMTP address for the group when using a custom domain.'
        )]
        [string]
        $PrimarySmtpAddress,
        [Parameter(
            Mandatory = $true,
            ParameterSetName = 'DefaultDomain',
            HelpMessage = 'Specifies the default domain to construct the primary SMTP address (alias@DefaultDomain) for the group.'
        )]
        [string]
        $DefaultDomain,
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Optional path to output the log file. If not provided, logs will not be written to a file.'
        )]
        [string]
        $LogOutputPath
    )
    if (-not $script:LogString) {
        Write-AuditLog -Start
    }
    else {
        Write-AuditLog -BeginFunction
    }
    try {
        if ($PSCmdlet.ShouldProcess("Creating or retrieving mail-enabled security group '$Name'")) {
            Connect-TkMsService -ExchangeOnline
            if (-not $Alias) {
                $Alias = $Name
            }
            if ($PSCmdlet.ParameterSetName -eq 'DefaultDomain') {
                $PrimarySmtpAddress = "$Alias@$DefaultDomain"
            }
            # Check if the distribution group already exists
            $existingGroup = Get-DistributionGroup -Identity $Name -ErrorAction SilentlyContinue
            if ($existingGroup) {
                # Confirm the group is security-enabled
                if ($existingGroup.GroupType -notmatch 'SecurityEnabled') {
                    throw "Group '$Name' exists but is not SecurityEnabled. Please provide a mail-enabled security group."
                }
                Write-AuditLog -Message "Distribution group '$Name' already exists. Returning existing group."
                return $existingGroup
            }
            # Create the distribution group
            $groupParams = @{
                Name               = $Name
                Alias              = $Alias
                PrimarySmtpAddress = $PrimarySmtpAddress
                Type               = 'security'
            }
            Write-AuditLog -Message "Creating distribution group with parameters: `n$($groupParams | Out-String)"
            $shouldProcessOperation = 'New-DistributionGroup'
            $shouldProcessTarget = "'$PrimarySmtpAddress'"
            if ($PSCmdlet.ShouldProcess($shouldProcessTarget, $shouldProcessOperation)) {
                $group = New-DistributionGroup @groupParams
                Write-AuditLog -Message "Distribution group created:`n$($group | Out-String)"
                return $group
            }
        }
    }
    catch {
        throw
    }
    finally {
        Write-AuditLog -EndFunction
        if ($LogOutputPath) {
            Write-AuditLog -End -OutputPath $LogOutputPath
        }
    }
}
#EndRegion '.\Public\New-MailEnabledSendingGroup.ps1' 132
#Region '.\Public\Publish-TkEmailApp.ps1' -1

<#
    .SYNOPSIS
        Publishes a new or existing Graph Email App with specified configurations.
    .DESCRIPTION
        The Publish-TkEmailApp function creates or configures a Graph Email App in Azure AD. It supports two scenarios:
        1. Creating a new app with specified parameters.
        2. Using an existing app and attaching a certificate to it.
    .PARAMETER AppPrefix
        The prefix used to initialize the Graph Email App. Must be 2-4 characters, letters, and numbers only. Default is 'Gtk'.
    .PARAMETER AuthorizedSenderUserName
        The username of the authorized sender. Must be a valid email address.
    .PARAMETER MailEnabledSendingGroup
        The mail-enabled security group. Must be a valid email address.
    .PARAMETER ExistingAppObjectId
        The AppId of the existing App Registration to which you want to attach a certificate. Must be a valid GUID.
    .PARAMETER CertPrefix
        Prefix to add to the certificate subject for the existing app.
    .PARAMETER CertThumbprint
        The thumbprint of the certificate to be retrieved. Must be a valid 40-character hexadecimal string.
    .PARAMETER KeyExportPolicy
        Key export policy for the certificate. Valid values are 'Exportable' and 'NonExportable'. Default is 'NonExportable'.
    .PARAMETER VaultName
        If specified, use a custom vault name. Otherwise, use the default 'GraphEmailAppLocalStore'.
    .PARAMETER OverwriteVaultSecret
        If specified, overwrite the vault secret if it already exists.
    .PARAMETER ReturnParamSplat
        If specified, return the parameter splat for use in other functions.
    .PARAMETER DoNotUseDomainSuffix
        Switch to add session domain suffix to the app name.
    .PARAMETER LogOutput
        If specified, log the output to the console.
    .EXAMPLE
        # Permissions required for app registration:
            - 'Application.ReadWrite.All'
            - 'DelegatedPermissionGrant.ReadWrite.All'
            - 'Directory.ReadWrite.All'
            - 'RoleManagement.ReadWrite.Directory'
        # Permissions granted to the app:
            - 'Mail.Send' (Application) - Send mail as any user
            # Exchange application policy restricts send to a mail enabled security group
        # Ensure a mail enabled sending group is created first:
            $DefaultDomain = 'contoso.com'
            $MailEnabledSendingGroupToCreate = "CTSO-GraphAPIMail"
        # Creates a mail-enabled security group named "MySenders" using a default domain
            $group = New-MailEnabledSendingGroup -Name $MailEnabledSendingGroupToCreate -DefaultDomain $DefaultDomain
        # Create a new Graph Email App for a single tenant
            $LicensedUserToSendAs = 'helpdesk@contoso.com'
            Publish-TkEmailApp `
                -AuthorizedSenderUserName $LicensedUserToSendAs `
                -MailEnabledSendingGroup $group.PrimarySmtpAddress `
                -ReturnParamSplat
        # Returns an app named like 'GraphToolKit-Gtk-<Session AD Domain>-As-helpdesk'
        # Returns a param splat that can be used as input for the send mail function:
        # Example:
            $params = @{
                AppId = 'your-app-id'
                Id = 'your-app-object-id'
                AppName = 'GraphToolKit-Gtk-<Session AD Domain>-As-helpdesk'
                CertificateSubject = 'GraphToolKit-GTK-<Session AD Domain>-As-helpdesk'
                AppRestrictedSendGroup = 'CTSO-GraphAPIMail@contoso.com'
                CertExpires = 'yyyy-MM-dd HH:mm:ss'
                CertThumbprint = 'your-cert-thumbprint'
                ConsentUrl = 'https://login.microsoftonline.com/<your-tenant-id>/adminconsent?client_id=<your-app-id>'
                DefaultDomain = 'contoso.com'
                SendAsUser = 'helpdesk'
                SendAsUserEmail = 'helpdesk@contoso.com'
                TenantID = 'your-tenant-id'
            }
    .EXAMPLE
        # Create a multi client app registration where one app exists and multiple certificates are associated to the app:
        # Initial setup:
        # Create the group as before (or reuse the existing group) and run the following commands:
            $LicensedUserToSendAs = 'helpdesk@contoso.com'
            $CertPrefix = "CTSO" # First Company prefix. This will be used to prefix the certificate subject.
            Publish-TkEmailApp `
                -CertPrefix $CertPrefix `
                -AuthorizedSenderUserName $LicensedUserToSendAs `
                -MailEnabledSendingGroup $group.PrimarySmtpAddress `
                -ReturnParamSplat
        # Returns an app named like 'GraphToolKit-Gtk-<Session AD Domain>-As-helpdesk'
            $params = @{
                AppId = 'your-app-id'
                Id = 'your-app-object-id'
                AppName = 'GraphToolKit-Gtk-<Session AD Domain>-As-helpdesk'
                CertificateSubject = 'GraphToolKit-CTSO-<Session AD Domain>-As-helpdesk'
                AppRestrictedSendGroup = 'CTSO-GraphAPIMail@contoso.com'
                CertExpires = 'yyyy-MM-dd HH:mm:ss'
                CertThumbprint = 'your-cert-thumbprint'
                ConsentUrl = 'https://login.microsoftonline.com/<your-tenant-id>/adminconsent?client_id=<your-app-id>'
                DefaultDomain = 'contoso.com'
                SendAsUser = 'helpdesk'
                SendAsUserEmail = 'helpdesk@contoso.com'
                TenantID = 'your-tenant-id'
            }
            $useExistingParams = @{
                ExistingAppObjectId = $params.Id
                CertPrefix = 'NewCompany'
                OverwriteVaultSecret = $true # optional, if you want to overwrite the existing vault secret
                ReturnParamSplat = $true # optional, returns the param splat
            }
            Publish-TkEmailApp @useExistingParams
        # The new Cert will be prefixed with the new company prefix and will allow the current client to authenticate.
        # Back in the app registrations console, if you look at the internal notes in the properties of the app:
        # The app's "Internal Notes" will be populated with the following json:
        # Assists in tracking the app's usage and configuration.
            {
                "GraphEmailAppFor": "helpdesk@contoso.com",
                "RestrictedToGroup": "CTSO-GraphAPIMail@contoso.com",
                "AppPermissions": "Mail.Send",
                "New-Company_ClientIP": "<Public IP Address of the client where the app was called>",
                "New-Company_Host": "<Host of the client where the app was called>",
                "NewCoolCompany_ClientIP": "<Public IP Address of the client where the app was called>",
                "NewCoolCompany_Host": "Host of the client where the app was called>"
            }
            # New cert additions added through the toolkit will append new client info to these notes.
    .NOTES
        This cmdlet requires that the user running the cmdlet have the necessary permissions to create the app and connect to Exchange Online.
#>

function Publish-TkEmailApp {
    [CmdletBinding(SupportsShouldProcess = $true , ConfirmImpact = 'High', DefaultParameterSetName = 'CreateNewApp')]
    param(
        # REGION: CREATE NEW APP param set
        [Parameter(
            Mandatory = $false,
            ParameterSetName = 'CreateNewApp',
            HelpMessage = `
                'The prefix used to initialize the Graph Email App. 2-4 characters letters and numbers only.'
        )]
        [ValidatePattern('^[A-Z0-9]{2,4}$')]
        [string]
        $AppPrefix = 'Gtk',
        [Parameter(
            Mandatory = $true,
            ParameterSetName = 'CreateNewApp',
            HelpMessage = `
                'The username of the authorized sender.'
        )]
        [ValidatePattern('^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$')]
        [string]
        $AuthorizedSenderUserName,
        [Parameter(
            Mandatory = $true,
            ParameterSetName = 'CreateNewApp',
            HelpMessage = `
                'The Mail Enabled Sending Group.'
        )]
        [ValidatePattern('^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$')]
        [string]
        $MailEnabledSendingGroup,
        # REGION: USE EXISTING APP param set
        [Parameter(
            Mandatory = $true,
            ParameterSetName = 'UseExistingApp',
            HelpMessage = `
                'The AppId of the existing App Registration to which you want to attach a certificate.'
        )]
        [ValidatePattern('^[0-9a-fA-F-]{36}$')]
        [string]
        $ExistingAppObjectId,
        [Parameter(
            Mandatory = $true,
            ParameterSetName = 'UseExistingApp',
            HelpMessage = `
                'Prefix to add to certificate subject for existing app.'
        )]
        [Parameter(
            Mandatory = $false,
            ParameterSetName = 'CreateNewApp',
            HelpMessage = `
                'Prefix to add to certificate subject for existing app.'
        )]
        [string]
        $CertPrefix,
        # REGION: Shared parameters
        [Parameter(
            Mandatory = $false,
            HelpMessage = `
                'The thumbprint of the certificate to be retrieved.'
        )]
        [ValidatePattern('^[A-Fa-f0-9]{40}$')]
        [string]
        $CertThumbprint,
        [Parameter(
            Mandatory = $false,
            HelpMessage = `
                'Key export policy for the certificate.'
        )]
        [ValidateSet('Exportable', 'NonExportable')]
        [string]
        $KeyExportPolicy = 'NonExportable',
        [Parameter(
            Mandatory = $false,
            HelpMessage = `
                'If specified, use a custom vault name. Otherwise, use the default.'
        )]
        [string]
        $VaultName = 'GraphEmailAppLocalStore',
        [Parameter(
            Mandatory = $false,
            HelpMessage = `
                'If specified, overwrite the vault secret if it already exists.'
        )]
        [switch]
        $OverwriteVaultSecret,
        [Parameter(
            Mandatory = $false,
            HelpMessage = `
                'Return the parameter splat for use in other functions.'
        )]
        [switch]
        $ReturnParamSplat,
        [Parameter(
            Mandatory = $false,
            HelpMessage = `
                'Switch to add session domain suffix to the app name.'
        )]
        [switch]
        $DoNotUseDomainSuffix,
        [Parameter(
            Mandatory = $false,
            HelpMessage = `
                'If specified, log the output to the console to the specified log file.'
        )]
        [string]
        $LogOutput
    )
    begin {
        <#
            This cmdlet requires that the user running the cmdlet have the necessary permissions to
            create the app and connect to Exchange Online. In addition, a mail-enabled security group
            must already exist in Exchange Online for the MailEnabledSendingGroup parameter.
            Permissions required:
                'Application.ReadWrite.All',
                'DelegatedPermissionGrant.ReadWrite.All',
                'Directory.ReadWrite.All',
                'RoleManagement.ReadWrite.Directory'
        #>

        if (-not $script:LogString) {
            Write-AuditLog -Start
        }
        else {
            Write-AuditLog -BeginFunction
        }
        try {
            Write-AuditLog '###############################################'
            # 1) Ensure required modules are installed
            $PublicMods = 'Microsoft.Graph', 'ExchangeOnlineManagement', 'Microsoft.PowerShell.SecretManagement', 'SecretManagement.JustinGrote.CredMan'
            $PublicVers = '1.22.0', '3.1.0', '1.1.2', '1.0.0'
            $ImportMods = 'Microsoft.Graph.Authentication', 'Microsoft.Graph.Applications', 'Microsoft.Graph.Identity.SignIns', 'Microsoft.Graph.Users'
            $ModParams = @{
                PublicModuleNames      = $PublicMods
                PublicRequiredVersions = $PublicVers
                ImportModuleNames      = $ImportMods
                Scope                  = 'CurrentUser'
            }
            Initialize-TkModuleEnv @ModParams
            $scopesNeeded = @(
                'Application.ReadWrite.All',
                'DelegatedPermissionGrant.ReadWrite.All',
                'Directory.ReadWrite.All'
            )
        }
        catch {
            throw
        }
    }
    process {
        $target = if ($AppPrefix) { $AppPrefix } else { $CertPrefix }
        $shouldProcessTarget = "Graph Email App $target"
        $shouldProcessOperation = 'Publish-TkEmailApp'
        if ($PSCmdlet.ShouldProcess($shouldProcessTarget, $shouldProcessOperation)) {
            switch ($PSCmdlet.ParameterSetName) {
                # ------------------------------------------------------
                # ============== SCENARIO 1: CREATE NEW APP =============
                # ------------------------------------------------------
                'CreateNewApp' {
                    # 2) Connect to both Graph and Exchange
                    Connect-TkMsService `
                        -MgGraph `
                        -ExchangeOnline `
                        -GraphAuthScopes $scopesNeeded
                    # 3) Grab MgContext for tenant info
                    $Context = Get-MgContext
                    if (!$Context) {
                        throw 'Could not retrieve the context for the tenant.'
                    }
                    # 1) Validate the user (AuthorizedSenderUserName) is in tenant
                    $user = Get-MgUser -Filter "Mail eq '$AuthorizedSenderUserName'"
                    if (-not $user) {
                        throw "User '$AuthorizedSenderUserName' not found in the tenant."
                    }
                    # 2) Build the app context (Mail.Send permission, etc.)
                    $AppSettings = Initialize-TkRequiredResourcePermissionObject `
                        -GraphPermissions 'Mail.Send'
                    $appName = Initialize-TkAppName `
                        -Prefix $AppPrefix `
                        -UserId $AuthorizedSenderUserName `
                        -DoNotUseDomainSuffix:$DoNotUseDomainSuffix `
                        -ErrorAction Stop
                    # Verify if the secret already exists in the vault
                    $existingSecret = Get-TkExistingSecret `
                        -AppName $appName `
                        -VaultName $VaultName `
                        -ErrorAction SilentlyContinue
                    if ($ExistingSecret -and -not $OverwriteVaultSecret) {
                        throw "Secret '$AppName' already exists in vault '$VaultName'. Use the -OverwriteVaultSecret switch to overwrite it."
                    }
                    # Add relevant properties
                    $AppSettings | Add-Member -NotePropertyName 'User' -NotePropertyValue $user
                    $AppSettings | Add-Member -NotePropertyName 'AppName' -NotePropertyValue $appName
                    if ($CertPrefix) {
                        $updatedString = $appName -replace '(GraphToolKit-)[A-Za-z0-9]{2,4}(?=-)', "`$1$CertPrefix"
                        $CertificateSubject = "CN=$updatedString"
                        $ClientCertPrefix = "$certPrefix"
                    }
                    else {
                        $CertificateSubject = "CN=$appName"
                        $ClientCertPrefix = "$AppPrefix"
                    }
                    # 3) Create or retrieve the certificate
                    $AppAuthCertificateParams = @{
                        AppName         = $AppSettings.AppName
                        Thumbprint      = $CertThumbprint
                        Subject         = $CertificateSubject
                        KeyExportPolicy = $KeyExportPolicy
                        ErrorAction     = 'Stop'
                    }
                    $CertDetails = Initialize-TkAppAuthCertificate @AppAuthCertificateParams
                    # 4) Show the proposed object
                    $proposedObject = [PSCustomObject]@{
                        ProposedAppName                 = $AppSettings.AppName
                        ProposedCertificateSubject      = $CertificateSubject
                        CertificateThumbprintUsed       = $CertDetails.CertThumbprint
                        CertExpires                     = $CertDetails.CertExpires
                        UserPrincipalName               = $user.UserPrincipalName
                        TenantID                        = $Context.TenantId
                        Permissions                     = 'Mail.Send'
                        PermissionType                  = 'Application'
                        ConsentType                     = 'AllPrincipals'
                        ExchangePolicyRestrictedToGroup = $MailEnabledSendingGroup
                    }
                    Write-AuditLog 'The following object will be created (or configured) in Azure AD:'
                    Write-AuditLog ($proposedObject | Format-List | Out-String)
                    # 5) Only proceed if ShouldProcess is allowed
                    try {
                        # Build a hashtable (or PSCustomObject) of the fields you want:
                        $notesHash = [ordered]@{
                            GraphEmailAppFor                  = $AuthorizedSenderUserName
                            RestrictedToGroup                 = $MailEnabledSendingGroup
                            AppPermissions                    = 'Mail.Send'
                            ($ClientCertPrefix + '_ClientIP') = (Invoke-RestMethod ifconfig.me/ip)
                            ($ClientCertPrefix + '_Host')     = $env:COMPUTERNAME
                        }
                        # Convert that hashtable to a JSON string:
                        $Notes = $notesHash | ConvertTo-Json #-Compress
                        # 6) Register the new enterprise app for Graph
                        $AppRegistrationParams = @{
                            DisplayName                = $AppSettings.AppName
                            CertThumbprint             = $CertDetails.CertThumbprint
                            RequiredResourceAccessList = $AppSettings.RequiredResourceAccessList
                            SignInAudience             = 'AzureADMyOrg'
                            Notes                      = $Notes
                            ErrorAction                = 'Stop'
                        }
                        $appRegistration = New-TkAppRegistration @AppRegistrationParams
                        # 7) Initialize the service principal, permissions, etc.
                        $AppSpRegistrationParams = @{
                            AppRegistration            = $appRegistration
                            Context                    = $Context
                            RequiredResourceAccessList = $AppSettings.RequiredResourceAccessList
                            Scopes                     = $permissionsObject
                            AuthMethod                 = 'Certificate'
                            CertThumbprint             = $CertDetails.CertThumbprint
                            ErrorAction                = 'Stop'
                        }
                        $ConsentUrl = New-TkAppSpOauth2Registration @AppSpRegistrationParams
                        [void](Read-Host 'Provide admin consent now, or copy the url and provide admin consent later. Press Enter to continue.')
                        # 8) Create the Exchange Online policy restricting send
                        New-TkExchangeEmailAppPolicy `
                            -AppRegistration $appRegistration `
                            -MailEnabledSendingGroup $MailEnabledSendingGroup `
                            -AuthorizedSenderUserName $AuthorizedSenderUserName
                        # 9) Build final output object
                        $EmailAppParams = @{
                            AppId                  = $appRegistration.AppId
                            Id                     = $appRegistration.Id
                            AppName                = "$($AppSettings.AppName)"
                            CertificateSubject     = $CertificateSubject
                            AppRestrictedSendGroup = $MailEnabledSendingGroup
                            CertExpires            = $CertDetails.CertExpires
                            CertThumbprint         = $CertDetails.CertThumbprint
                            ConsentUrl             = $ConsentUrl
                            DefaultDomain          = $MailEnabledSendingGroup.Split('@')[1]
                            SendAsUser             = $AppSettings.User.UserPrincipalName.Split('@')[0]
                            SendAsUserEmail        = $AppSettings.User.UserPrincipalName
                            TenantID               = $Context.TenantId
                        }
                        [TkEmailAppParams]$graphEmailApp = Initialize-TkEmailAppParamsObject @EmailAppParams
                        # 10) Store it as JSON in the vault
                        $JsonSecretParams = @{
                            Name        = "CN=$($AppSettings.AppName)"
                            InputObject = $graphEmailApp
                            VaultName   = $VaultName
                            Overwrite   = $OverwriteVaultSecret
                            ErrorAction = 'Stop'
                        }
                        $savedSecretName = Set-TkJsonSecret @JsonSecretParams
                        Write-AuditLog "Secret '$savedSecretName' saved to vault '$VaultName'."
                    }
                    catch {
                        throw
                    }
                }
                # ---------------------------------------------------------
                # ============ SCENARIO 2: USE EXISTING APP ===============
                # ---------------------------------------------------------
                'UseExistingApp' {
                    # Grab MgContext for tenant info
                    Connect-TkMsService `
                        -MgGraph `
                        -GraphAuthScopes $scopesNeeded
                    $Context = Get-MgContext
                    if (!$Context) {
                        throw 'Could not retrieve the context for the tenant.'
                    }
                    $ClientCertPrefix = "$CertPrefix"
                    # Retrieve the existing app registration by AppId
                    Write-AuditLog "Looking up existing app with ObjectId: $ExistingAppObjectId"
                    # Get-MgApplication uses the application object id, not the app id
                    $existingApp = Get-MgApplication -ApplicationId $ExistingAppObjectId -ErrorAction Stop
                    if (-not $existingApp) {
                        throw "Could not find an existing application with AppId '$ExistingAppObjectId'."
                    }
                    if (!($existingApp | Where-Object { $_.DisplayName -like 'GraphToolKit-*' })) {
                        throw "The existing app with AppId '$ExistingAppObjectId' is not a GraphToolKit app."
                    }
                    $updatedString = $existingApp.DisplayName -replace '(GraphToolKit-)[A-Za-z0-9]{2,4}(?=-)', "`$1$CertPrefix"
                    # Retrieve or create the certificate
                    $certParams = @{
                        AppName         = $updatedString
                        Thumbprint      = $CertThumbprint
                        Subject         = "CN=$updatedString"
                        KeyExportPolicy = $KeyExportPolicy
                        ErrorAction     = 'Stop'
                    }
                    $certDetails = Initialize-TkAppAuthCertificate @certParams
                    Write-AuditLog "Attaching certificate (Thumbprint: $($certDetails.CertThumbprint)) to existing app '$($existingApp.DisplayName)'."
                    # Merge or append the new certificate to the existing KeyCredentials
                    $currentKeys = $existingApp.KeyCredentials
                    $newCert = @{
                        Type        = 'AsymmetricX509Cert'
                        Usage       = 'Verify'
                        Key         = (Get-ChildItem -Path Cert:\CurrentUser\My |
                            Where-Object { $_.Thumbprint -eq $certDetails.CertThumbprint }).RawData
                        DisplayName = "CN=$updatedString"
                    }
                    # If you want to specify start/end date, you can do so as well:
                    # $newCert.StartDateTime = (Get-Date)
                    # $newCert.EndDateTime = (Get-Date).AddYears(1)
                    # Append the new cert to existing
                    $mergedKeys = $currentKeys + $newCert
                    $existingNotesRaw = $existingApp.Notes
                    if (-not [string]::IsNullOrEmpty($existingNotesRaw)) {
                        try {
                            $notesObject = $existingNotesRaw | ConvertFrom-Json -ErrorAction Stop
                        }
                        catch {
                            Write-AuditLog 'Existing .Notes was not valid JSON; ignoring it.'
                            $notesObject = [ordered]@{}
                        }
                    }
                    else {
                        $notesObject = [ordered]@{}
                    }
                    # Add your new properties each time the function runs
                    $notesObject | Add-Member -NotePropertyName ($clientCertPrefix + '_ClientIP') -NotePropertyValue (Invoke-RestMethod ifconfig.me/ip)
                    $notesObject | Add-Member -NotePropertyName ($clientCertPrefix + '_Host') -NotePropertyValue $env:COMPUTERNAME
                    $updatedNotes = $notesObject | ConvertTo-Json #-Compress
                    if (($updatedNotes.length -gt 1024)) {
                        throw 'The Notes object is too large. Please reduce the size of the Notes object.'
                    }
                    try {
                        # Update the application with the new KeyCredentials array
                        $updateAppParams = @{
                            ApplicationId  = $existingApp.Id
                            KeyCredentials = $mergedKeys
                            Notes          = $updatedNotes
                            ErrorAction    = 'Stop'
                        }
                        Update-MgApplication @updateAppParams | Out-Null
                        # Build an output object similar to "new" scenario
                        $emailAppParams = @{
                            AppId                  = $existingApp.AppId
                            Id                     = $existingApp.Id
                            AppName                = "$updatedString"
                            CertificateSubject     = "CN=$updatedString"
                            AppRestrictedSendGroup = $notesObject.RestrictedToGroup
                            CertExpires            = $certDetails.CertExpires
                            CertThumbprint         = $certDetails.CertThumbprint
                            ConsentUrl             = $null
                            DefaultDomain          = ($notesObject.GraphEmailAppFor.Split('@')[1])
                            SendAsUser             = ($notesObject.GraphEmailAppFor.Split('@')[0])
                            SendAsUserEmail        = $notesObject.GraphEmailAppFor
                            TenantID               = $context.TenantId
                        }
                        [TkEmailAppParams]$graphEmailApp = Initialize-TkEmailAppParamsObject @emailAppParams
                        # Store updated info in the vault
                        $jsonSecretParams = @{
                            Name        = "CN=$updatedString"
                            InputObject = $graphEmailApp
                            VaultName   = $VaultName
                            Overwrite   = $OverwriteVaultSecret
                            ErrorAction = 'Stop'
                        }
                        $savedSecretName = Set-TkJsonSecret @JsonSecretParams
                        Write-AuditLog "Secret for existing app saved as '$savedSecretName' in vault '$VaultName'."
                    }
                    catch {
                        throw
                    }
                }
            } # end switch
        }

    }
    end {
        if ($ReturnParamSplat -and $graphEmailApp) {
            return ($graphEmailApp | ConvertTo-ParameterSplat)
        }
        elseif ($graphEmailApp) {
            return $graphEmailApp
        }
        if ($LogOutput) {
            Write-AuditLog -End -LogOutput $LogOutput
        }
    }
}

#EndRegion '.\Public\Publish-TkEmailApp.ps1' 539
#Region '.\Public\Publish-TkM365AuditApp.ps1' -1

<#
    .SYNOPSIS
        Publishes (creates) a new M365 Audit App registration in Entra ID (Azure AD) with a specified certificate.
    .DESCRIPTION
        The Publish-TkM365AuditApp function creates a new Azure AD application used for M365 auditing.
        It connects to Microsoft Graph, gathers the required permissions for SharePoint and Exchange,
        and optionally creates a self-signed certificate if no thumbprint is provided. It also assigns
        the application to the Exchange Administrator and Global Reader roles. By default, the newly
        created application details are stored as a secret in the specified SecretManagement vault.
    .PARAMETER AppPrefix
        A short prefix (2-4 alphanumeric characters) used to build the app name. Defaults to "Gtk"
        if not specified. Example app name: GraphToolKit-MSN-GraphApp-MyDomain-As-helpDesk
    .PARAMETER CertThumbprint
        The thumbprint of an existing certificate in the current user's certificate store. If not
        provided, a new self-signed certificate is created.
    .PARAMETER KeyExportPolicy
        Specifies whether the newly created certificate (if no thumbprint is provided) is
        'Exportable' or 'NonExportable'. Defaults to 'NonExportable'.
    .PARAMETER VaultName
        The SecretManagement vault name in which to store the app credentials. Defaults to
        "M365AuditAppLocalStore" if not specified.
    .PARAMETER OverwriteVaultSecret
        If specified, overwrites an existing secret in the specified vault if it already exists.
    .PARAMETER ReturnParamSplat
        If specified, returns a parameter splat string for use in other functions, instead of the
        default PSCustomObject containing the app details.
    .PARAMETER DoNotUseDomainSuffix
        If specified, does not append the domain suffix to the app name.
    .EXAMPLE
        PS C:\> Publish-TkM365AuditApp -AppPrefix "CS12" -ReturnParamSplat
        Creates a new M365 Audit App with the prefix "CS12", returns a parameter splat, and stores
        the credentials in the default vault.
    .INPUTS
        None. This function does not accept pipeline input.
    .OUTPUTS
        By default, returns a PSCustomObject with details of the new app (AppId, ObjectId, TenantId,
        certificate thumbprint, expiration, etc.). If -ReturnParamSplat is used, returns a parameter
        splat string.
    .NOTES
        Requires the Microsoft.Graph and ExchangeOnlineManagement modules for app creation and
        role assignment. The user must have sufficient privileges to create and manage applications
        in Azure AD, and to assign roles. After creation, admin consent may be required for the
        assigned permissions.
        Permissions required for app registration:
            'Application.ReadWrite.All',
            'DelegatedPermissionGrant.ReadWrite.All',
            'Directory.ReadWrite.All',
            'RoleManagement.ReadWrite.Directory'
 
        Permissions granted to the app:
        (Exchange Administrator and Global Reader Roles are also added to the service principal.)
            'AppCatalog.ReadWrite.All',
            'Channel.Delete.All',
            'ChannelMember.ReadWrite.All',
            'ChannelSettings.ReadWrite.All',
            'Directory.Read.All',
            'Group.ReadWrite.All',
            'Organization.Read.All',
            'Policy.Read.All',
            'Domain.Read.All',
            'TeamSettings.ReadWrite.All',
            'User.Read.All',
            'Sites.Read.All',
            'Sites.FullControl.All',
            'Exchange.ManageAsApp'
#>

function Publish-TkM365AuditApp {
    [CmdletBinding(ConfirmImpact = 'High')]
    param(
        [Parameter(
            Mandatory = $false,
            HelpMessage = `
                'Prefix for the new M365 Audit app name (2-4 alphanumeric characters).'
        )]
        [ValidatePattern('^[A-Z0-9]{2,4}$')]
        [string]
        $AppPrefix = 'Gtk',
        [Parameter(
            Mandatory = $false,
            HelpMessage = `
                'Thumbprint of an existing certificate to use. If not provided, a self-signed cert will be created.'
        )]
        [ValidatePattern('^[A-Fa-f0-9]{40}$')]
        [string]
        $CertThumbprint,
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Key export policy for the certificate.'
        )]
        [ValidateSet('Exportable', 'NonExportable')]
        [string]
        $KeyExportPolicy = 'NonExportable',
        [Parameter(
            Mandatory = $false,
            HelpMessage = `
                'Name of the SecretManagement vault to store app credentials.'
        )]
        [string]
        $VaultName = 'M365AuditAppLocalStore',
        [Parameter(
            Mandatory = $false,
            HelpMessage = `
                'If specified, overwrite the vault secret if it already exists.'
        )]
        [switch]
        $OverwriteVaultSecret,
        [Parameter(
            Mandatory = $false,
            HelpMessage = `
                'Return output as a parameter splat string for use in other functions.'
        )]
        [switch]$ReturnParamSplat,
        [Parameter(
            Mandatory = $false,
            HelpMessage = `
                'If specified, do not append the domain suffix to the app name.'
        )]
        [switch]$DoNotUseDomainSuffix
    )
    begin {
        if (-not $script:LogString) {
            Write-AuditLog -Start
        }
        else {
            Write-AuditLog -BeginFunction
        }
        Write-AuditLog '###############################################'
        Write-AuditLog 'Initializing M365 Audit App publication process...'
        $scopesNeeded = @(
            'Application.ReadWrite.All',
            'DelegatedPermissionGrant.ReadWrite.All',
            'Directory.ReadWrite.All',
            'RoleManagement.ReadWrite.Directory'
        )
        # 1) Connect to Graph so we can query permissions & create the app
        Connect-TkMsService -MgGraph -GraphAuthScopes $scopesNeeded
    }
    process {
        try {
            # 2) Define read-only vs. read-write sets
            $graph = @(
                'AppCatalog.ReadWrite.All',
                'Channel.Delete.All',
                'ChannelMember.ReadWrite.All',
                'ChannelSettings.ReadWrite.All',
                'Directory.Read.All',
                'Group.ReadWrite.All',
                'Organization.Read.All',
                'Policy.Read.All',
                'Domain.Read.All'
                'TeamSettings.ReadWrite.All'
                'User.Read.All'
            )
            $sharePoint = @('Sites.Read.All', 'Sites.FullControl.All')
            $exchange = @('Exchange.ManageAsApp')
            # Decide which sets to use
            $permissionsObject = [PSCustomObject]@{
                Graph      = $graph
                SharePoint = $sharePoint
                Exchange   = $exchange
            }
            Write-AuditLog "Graph Perms: $($graph -join ', ')"
            Write-AuditLog "SharePoint Perms: $($sharePoint -join ', ')"
            Write-AuditLog "Exchange Perms: $($exchange -join ', ')"
            $Context = Get-MgContext -ErrorAction Stop
            # Gather the resource access objects (GUIDs) for all these perms
            $AppSettings = Initialize-TkRequiredResourcePermissionObject `
                -GraphPermissions $graph `
                -Scenario '365Audit' `
                -ErrorAction Stop
            # Generate the app name
            $appName = Initialize-TkAppName `
                -Prefix $AppPrefix `
                -ScenarioName 'M365Audit' `
                -DoNotUseDomainSuffix:$DoNotUseDomainSuffix `
                -ErrorAction Stop
            Write-AuditLog "Proposed new M365 Audit App name: $appName"
            # Retrieve or create the certificate
            $AppAuthCertificateParams = @{
                AppName         = $appName
                Thumbprint      = $CertThumbprint
                Subject         = "CN=$appName"
                KeyExportPolicy = $KeyExportPolicy
                ErrorAction     = 'Stop'
            }
            $CertDetails = Initialize-TkAppAuthCertificate @AppAuthCertificateParams
            Write-AuditLog "Certificate Thumbprint: $($CertDetails.CertThumbprint); Expires: $($CertDetails.CertExpires)."
            # Show user proposed config
            $proposed = [PSCustomObject]@{
                ProposedAppName       = $appName
                CertificateThumbprint = $CertDetails.CertThumbprint
                CertExpires           = $CertDetails.CertExpires
                GraphPermissions      = $graph -join ', '
                SharePointPermissions = $sharePoint -join ', '
                ExchangePermissions   = $exchange -join ', '
            }
            Write-AuditLog 'Proposed creation of a new M365 Audit App with the following properties:'
            Write-AuditLog "$($proposed | Format-List)"
            # Create the app in one pass with all resources
            $notesHash = [ordered]@{
                'Certificate Thumbprint'   = $($CertDetails.CertThumbprint)
                'Certificate Expires'      = $($CertDetails.CertExpires)
                'GraphAppPermissions'      = $($graph -join ', ')
                'SharePointAppPermissions' = $($sharePoint -join ', ')
                'ExchangeAppPermissions'   = $($exchange -join ', ')
                'RolesAssigned'            = @('Exchange Administrator', 'Global Reader')
                'AuthorizedClient IP'      = $((Invoke-RestMethod ifconfig.me/ip))
                'ClientOrUserHostname'     = if ($env:COMPUTERNAME) { $env:COMPUTERNAME } else { $env:USERNAME }
            }
            # Convert that hashtable to a JSON string:
            $Notes = $notesHash | ConvertTo-Json #-Compress
            Write-AuditLog 'Creating new EntraAD application with all resource permissions...'
            $AppRegistrationParams = @{
                DisplayName                = $appName
                CertThumbprint             = $CertDetails.CertThumbprint
                RequiredResourceAccessList = $AppSettings.RequiredResourceAccessList
                SignInAudience             = 'AzureADMyOrg'
                Notes                      = $Notes
                ErrorAction                = 'Stop'
            }
            $appRegistration = New-TkAppRegistration @AppRegistrationParams
            Write-AuditLog "App registered. Object ID = $($appRegistration.Id), ClientId = $($appRegistration.AppId)."
            # Grant the oauth2 permissions to service principal
            $AppSpRegistrationParams = @{
                AppRegistration            = $appRegistration
                Context                    = $Context
                RequiredResourceAccessList = $AppSettings.RequiredResourceAccessList
                Scopes                     = $permissionsObject
                AuthMethod                 = 'Certificate'
                CertThumbprint             = $CertDetails.CertThumbprint
                ErrorAction                = 'Stop'
            }
            $ConsentUrl = New-TkAppSpOauth2Registration @AppSpRegistrationParams
            [void](Read-Host 'Provide admin consent now, or copy the url and provide admin consent later. Press Enter to continue.')
            Write-AuditLog 'Appending Exchange Administrator role to the app.'
            $exoAdminRole = Get-MgDirectoryRole -Filter "displayName eq 'Exchange Administrator'" -ErrorAction Stop
            # Get the service principal object ID of the app
            $sp = Get-MgServicePrincipal -Filter "appId eq '$($appRegistration.appid)'" -ErrorAction Stop
            $spObjectId = $sp.Id
            $body = @{
                '@odata.id' = "https://graph.microsoft.com/v1.0/directoryObjects/$spObjectId"
            }
            New-MgDirectoryRoleMemberByRef `
                -DirectoryRoleId $exoAdminRole.Id `
                -BodyParameter $body `
                -ErrorAction Stop
            Write-AuditLog 'Appending Global Reader role to the app.'
            $globalReaderRole = Get-MgDirectoryRole `
                -Filter "displayName eq 'Global Reader'" `
                -ErrorAction Stop
            New-MgDirectoryRoleMemberByRef `
                -DirectoryRoleId $globalReaderRole.Id `
                -BodyParameter $body `
                -ErrorAction Stop
            # Store final app info in the vault
            $M365AuditAppParams = @{
                AppName               = "CN=$appName"
                AppId                 = $appRegistration.AppId
                ObjectId              = $appRegistration.Id
                TenantId              = $context.TenantId
                CertThumbprint        = $CertDetails.CertThumbprint
                CertExpires           = $CertDetails.CertExpires
                ConsentUrl            = $ConsentUrl
                MgGraphPermissions    = "$graph"
                SharePointPermissions = "$sharePoint"
                ExchangePermissions   = "$exchange"
            }
            [TkM365AuditAppParams]$m365AuditApp = Initialize-TkM365AuditAppParamsObject @M365AuditAppParams
            # Save to vault
            $JsonSecretParams = @{
                Name        = "CN=$appName"
                InputObject = $m365AuditApp
                VaultName   = $VaultName
                Overwrite   = $OverwriteVaultSecret
                ErrorAction = 'Stop'
            }
            $savedName = Set-TkJsonSecret @JsonSecretParams
            Write-AuditLog "Secret '$savedName' saved to vault '$VaultName'."
            # Return as either param splat or plain object
            if ($ReturnParamSplat) {
                return $m365AuditApp | ConvertTo-ParameterSplat
            }
            else {
                return $m365AuditApp
            }

        }
        catch {
            throw
        }
        finally {
            Write-AuditLog -EndFunction
        }
    }
}
#EndRegion '.\Public\Publish-TkM365AuditApp.ps1' 296
#Region '.\Public\Publish-TkMemPolicyManagerApp.ps1' -1

<#
    .SYNOPSIS
        Publishes a new MEM (Intune) Policy Manager App in Azure AD with read-only or read-write permissions.
    .DESCRIPTION
        The Publish-TkMemPolicyManagerApp function creates an Azure AD application intended for managing
        Microsoft Endpoint Manager (MEM/Intune) policies. It optionally creates or retrieves a certificate,
        configures the necessary Microsoft Graph permissions for read-only or read-write access, and stores
        the resulting app credentials in a SecretManagement vault.
    .PARAMETER AppPrefix
        A 2-4 character prefix used to build the application name (e.g., CORP, MSN). This helps uniquely
        identify the app in Azure AD.
    .PARAMETER CertThumbprint
        The thumbprint of an existing certificate in the current user's certificate store. If omitted,
        a new self-signed certificate is created.
    .PARAMETER KeyExportPolicy
        Specifies whether the newly created certificate is 'Exportable' or 'NonExportable'.
        Defaults to 'NonExportable' if not specified.
    .PARAMETER VaultName
        The name of the SecretManagement vault in which to store the app credentials.
        Defaults to 'MemPolicyManagerLocalStore'.
    .PARAMETER OverwriteVaultSecret
        If specified, overwrites any existing secret of the same name in the vault.
    .PARAMETER ReadWrite
        If specified, grants read-write MEM/Intune permissions. Otherwise, read-only permissions are granted.
    .PARAMETER ReturnParamSplat
        If specified, returns a parameter splat string for use in other functions. Otherwise, returns
        a PSCustomObject containing the app details.
    .PARAMETER DoNotUseDomainSuffix
        If specified, the function does not append the domain suffix to the app name.
    .EXAMPLE
        PS C:\> Publish-TkMemPolicyManagerApp -AppPrefix "CORP" -ReadWrite
        Creates a new MEM Policy Manager App with read-write permissions, retrieves or
        creates a certificate, and stores the credentials in the default vault.
    .INPUTS
        None. This function does not accept pipeline input.
    .OUTPUTS
        By default, returns a PSCustomObject (TkMemPolicyManagerAppParams) with details of the newly created
        app (AppId, certificate thumbprint, tenant ID, etc.). If -ReturnParamSplat is used, returns a parameter
        splat string.
    .NOTES
        This function requires the Microsoft.Graph module for application creation and the user must have
        permissions in Azure AD to register and grant permissions to the application. After creation, admin
        consent may be needed to finalize the permission grants.
        Permissions required for app registration::
            'Application.ReadWrite.All',
            'DelegatedPermissionGrant.ReadWrite.All',
            'Directory.ReadWrite.All'
 
        Permissions required for read-only access:
 
            'DeviceManagementConfiguration.Read.All',
            'DeviceManagementApps.Read.All',
            'DeviceManagementManagedDevices.Read.All',
            'Policy.Read.ConditionalAccess',
            'Policy.Read.All'
 
        Permissions required for read-write access:
            'DeviceManagementConfiguration.ReadWrite.All',
            'DeviceManagementApps.ReadWrite.All',
            'DeviceManagementManagedDevices.ReadWrite.All',
            'Policy.ReadWrite.ConditionalAccess',
            'Policy.Read.All'
#>

function Publish-TkMemPolicyManagerApp {
    [CmdletBinding(ConfirmImpact = 'High')]
    param(
        [Parameter(
            Mandatory = $true,
            HelpMessage = `
                '2-4 character prefix used for the App Name (e.g. MSN, CORP, etc.)'
        )]
        [ValidatePattern('^[A-Z0-9]{2,4}$')]
        [string]
        $AppPrefix,
        [Parameter(
            Mandatory = $false,
            HelpMessage = `
                'Thumbprint of the certificate. If omitted, a self-signed cert is created.'
        )]
        [ValidatePattern('^[A-Fa-f0-9]{40}$')]
        [string]
        $CertThumbprint,
        [Parameter(
            Mandatory = $false,
            HelpMessage = `
                'Key export policy for the certificate.'
        )]
        [ValidateSet('Exportable', 'NonExportable')]
        [string]
        $KeyExportPolicy = 'NonExportable',
        [Parameter(
            Mandatory = $false,
            HelpMessage = `
                'If specified, use a custom vault name. Otherwise, use the default.'
        )]
        [string]
        $VaultName = 'MemPolicyManagerLocalStore',
        [Parameter(
            Mandatory = $false,
            HelpMessage = `
                'If specified, overwrite the vault secret if it already exists.'
        )]
        [switch]
        $OverwriteVaultSecret,
        [Parameter(
            HelpMessage = `
                'If specified, grant ReadWrite perms. Otherwise, read-only perms.'
        )]
        [switch]
        $ReadWrite,
        [Parameter(
            Mandatory = $false,
            HelpMessage = `
                'Return the param splat for use in other functions.'
        )]
        [switch]$ReturnParamSplat,
        [Parameter(
            Mandatory = $false,
            HelpMessage = `
                'If specified, do not append the domain suffix to the app name.'
        )]
        [switch]
        $DoNotUseDomainSuffix
    )
    begin {
        if (-not $script:LogString) {
            Write-AuditLog -Start
        }
        else {
            Write-AuditLog -BeginFunction
        }
        try {
            Write-AuditLog '###############################################'
            $PublicMods = 'Microsoft.Graph', 'Microsoft.PowerShell.SecretManagement', 'SecretManagement.JustinGrote.CredMan'
            $PublicVers = '1.22.0', '1.1.2', '1.0.0'
            $ImportMods = 'Microsoft.Graph.Authentication', 'Microsoft.Graph.Applications', 'Microsoft.Graph.Identity.SignIns', 'Microsoft.Graph.Users'
            $ModParams = @{
                PublicModuleNames      = $PublicMods
                PublicRequiredVersions = $PublicVers
                ImportModuleNames      = $ImportMods
                Scope                  = 'CurrentUser'
            }
            Initialize-TkModuleEnv @ModParams
            # Only connect to Graph
            $scopesNeeded = @(
                'Application.ReadWrite.All',
                'DelegatedPermissionGrant.ReadWrite.All',
                'Directory.ReadWrite.All'
            )
            Connect-TkMsService `
                -MgGraph `
                -GraphAuthScopes $scopesNeeded `
                -ErrorAction Stop
            $Context = Get-MgContext -ErrorAction Stop
        }
        catch {
            throw
        }
    }
    process {
        try {
            # 1) Determine the correct set of MEM permissions
            $readWritePerms = @(
                'DeviceManagementConfiguration.ReadWrite.All',
                'DeviceManagementApps.ReadWrite.All',
                'DeviceManagementManagedDevices.ReadWrite.All',
                'Policy.ReadWrite.ConditionalAccess',
                'Policy.Read.All'
            )
            $readOnlyPerms = @(
                'DeviceManagementConfiguration.Read.All',
                'DeviceManagementApps.Read.All',
                'DeviceManagementManagedDevices.Read.All',
                'Policy.Read.ConditionalAccess',
                'Policy.Read.All'
            )
            $permissions = if ($ReadWrite) { $readWritePerms } else { $readOnlyPerms }
            $permissionsObject = [PSCustomObject]@{
                Graph = $permissions
            }
            Write-AuditLog "Using the following MEM permissions: $($permissions -join ', ')"
            # 2) Build a Graph context object that looks up these permission IDs
            $AppSettings = Initialize-TkRequiredResourcePermissionObject `
                -GraphPermissions $permissions
            # 3) Build an app name for scenario "MemPolicyManager"
            $appName = Initialize-TkAppName `
                -Prefix $AppPrefix `
                -ScenarioName 'MemPolicyManager' `
                -DoNotUseDomainSuffix:$DoNotUseDomainSuffix `
                -ErrorAction Stop
            # 4) Add TenantId & AppName to the object so we can store them in the final JSON
            $AppSettings | Add-Member -NotePropertyName 'TenantId' -NotePropertyValue $Context.TenantId
            $AppSettings | Add-Member -NotePropertyName 'AppName' -NotePropertyValue $appName
            # 5) Create or retrieve the certificate
            $AppAuthCertificateParams = @{
                AppName         = $AppSettings.AppName
                Thumbprint      = $CertThumbprint
                Subject         = "CN=$($AppSettings.AppName)"
                KeyExportPolicy = $KeyExportPolicy
                ErrorAction     = 'Stop'
            }
            $CertDetails = Initialize-TkAppAuthCertificate @AppAuthCertificateParams
            # Build a “proposed” object so the user sees what’s about to happen
            $proposedObject = [PSCustomObject]@{
                ProposedAppName           = $AppSettings.AppName
                CertificateThumbprintUsed = $CertDetails.CertThumbprint
                CertificateExpires        = $CertDetails.CertExpires
                TenantID                  = $Context.TenantId
                RequestedPermissions      = ($permissions -join ', ')
                PermissionType            = 'Application'
            }
            Write-AuditLog 'Proposed creation of a new MEM Policy Manager App with the following properties:'
            Write-AuditLog "$($proposedObject | Format-List)"
            $notesHash = [ordered]@{
                'Certificate Thumbprint' = $($CertDetails.CertThumbprint)
                'Certificate Expires'    = $($CertDetails.CertExpires)
                'GraphAppPermissions'    = $($permissions -join ', ')
                'Read-Write Permissions' = $(if ($ReadWrite) { 'ReadWrite' } else { 'Read-Only' })
                'AuthorizedClient IP'    = $((Invoke-RestMethod ifconfig.me/ip))
                'ClientOrUserHostname'   = if ($env:COMPUTERNAME) { $env:COMPUTERNAME } else { $env:USERNAME }
            }
            # Convert that hashtable to a JSON string:
            $Notes = $notesHash | ConvertTo-Json #-Compress
            # 6) Register the application (with the cert)
            $AppRegistrationParams = @{
                DisplayName                = $AppSettings.AppName
                CertThumbprint             = $CertDetails.CertThumbprint
                RequiredResourceAccessList = $AppSettings.RequiredResourceAccessList
                SignInAudience             = 'AzureADMyOrg'
                Notes                      = $Notes
                ErrorAction                = 'Stop'
            }
            $appRegistration = New-TkAppRegistration @AppRegistrationParams
            # 7) Create the Service Principal & grant the permissions
            $AppSpRegistrationParams = @{
                AppRegistration            = $appRegistration
                Context                    = $Context
                RequiredResourceAccessList = $AppSettings.RequiredResourceAccessList
                Scopes                     = $permissionsObject
                AuthMethod                 = 'Certificate'
                CertThumbprint             = $CertDetails.CertThumbprint
                ErrorAction                = 'Stop'
            }
            $ConsentUrl = New-TkAppSpOauth2Registration @AppSpRegistrationParams
            [void](Read-Host 'Provide admin consent now, or copy the url and provide admin consent later. Press Enter to continue.')
            # 8) Build a final PSCustomObject to store in the secret vault
            $TkMemPolicyManagerAppParams = @{
                AppId          = $appRegistration.AppId
                AppName        = "CN=$($AppSettings.AppName)"
                CertThumbprint = $CertDetails.CertThumbprint
                ObjectId       = $appRegistration.Id
                ConsentUrl     = $ConsentUrl
                PermissionSet  = if ($ReadWrite) { 'ReadWrite' } else { 'ReadOnly' }
                Permissions    = $permissions
                TenantId       = $Context.TenantId
            }
            [TkMemPolicyManagerAppParams]$AppParamsObject = Initialize-TkMemPolicyManagerAppParamsObject @TkMemPolicyManagerAppParams
            # 9) Store as JSON secret
            $JsonSecretParams = @{
                Name        = "CN=$($AppSettings.AppName)"
                InputObject = $AppParamsObject
                VaultName   = $VaultName
                Overwrite   = $OverwriteVaultSecret
                ErrorAction = 'Stop'
            }
            $savedName = Set-TkJsonSecret @JsonSecretParams
            Write-AuditLog "Secret '$savedName' saved to vault '$VaultName'."
            # Return the final object (param-splat or normal)
            if ($ReturnParamSplat) {
                return $AppParamsObject | ConvertTo-ParameterSplat
            }
            else {
                return $AppParamsObject
            }
        }
        catch {
            throw
        }
    }
    end {
        Write-AuditLog -EndFunction
    }
}
#EndRegion '.\Public\Publish-TkMemPolicyManagerApp.ps1' 284
#Region '.\Public\Send-TkEmailAppMessage.ps1' -1

<#
    .SYNOPSIS
        Sends an email using the Microsoft Graph API, either by retrieving app credentials from a local vault
        or by specifying them manually.
    .DESCRIPTION
        The Send-TkEmailAppMessage function uses the Microsoft Graph API to send an email to a specified
        recipient. It supports two parameter sets:
        1. 'Vault' (default): Provide an existing app name (AppName) whose credentials are stored in the
            local secret vault (e.g., GraphEmailAppLocalStore). The function retrieves the AppId, TenantId,
            and certificate thumbprint automatically.
        2. 'Manual': Provide the AppId, TenantId, and certificate thumbprint yourself, bypassing the vault.
            In both cases, the function obtains an OAuth2 token (via MSAL.PS) using the specified certificate
            and uses the Microsoft Graph 'sendMail' endpoint to deliver the message.
    .PARAMETER AppName
        [Vault Parameter Set Only]
        The name of the pre-created Microsoft Graph Email App (stored in GraphEmailAppLocalStore). Used only
        if the 'Vault' parameter set is chosen. The function retrieves the AppId, TenantId, and certificate
        thumbprint from the vault entry.
    .PARAMETER AppId
        [Manual Parameter Set Only]
        The Azure AD application (client) ID to use for sending the email. Must be used together with TenantId
        and CertThumbprint in the 'Manual' parameter set.
    .PARAMETER TenantId
        [Manual Parameter Set Only]
        The Azure AD tenant ID (GUID or domain name). Must be used together with AppId and CertThumbprint
        in the 'Manual' parameter set.
    .PARAMETER CertThumbprint
        [Manual Parameter Set Only]
        The certificate thumbprint (in Cert:\CurrentUser\My) used for authenticating as the Azure AD app.
        Must be used together with AppId and TenantId in the 'Manual' parameter set.
    .PARAMETER To
        The email address of the recipient.
    .PARAMETER FromAddress
        The email address of the sender who is authorized to send email as configured in the Graph Email App.
    .PARAMETER Subject
        The subject line of the email.
    .PARAMETER EmailBody
        The body text of the email.
    .PARAMETER AttachmentPath
        An array of file paths for any attachments to include in the email. Each path must exist as a leaf file.
    .PARAMETER VaultName
        [Vault Parameter Set Only]
        The name of the vault to retrieve the GraphEmailApp object. Default is 'GraphEmailAppLocalStore'.
    .EXAMPLE
        # Using the 'Vault' parameter set
        Send-TkEmailAppMessage -AppName "GraphEmailApp" -To "recipient@example.com" -FromAddress "sender@example.com" `
            -Subject "Test Email" -EmailBody "This is a test email."
        Retrieves the app's credentials (AppId, TenantId, CertThumbprint) from the local vault under the
        secret name "GraphEmailApp" and sends an email.
    .EXAMPLE
        # Using the 'Manual' parameter set
        Send-TkEmailAppMessage -AppId "00000000-1111-2222-3333-444444444444" -TenantId "contoso.onmicrosoft.com" `
            -CertThumbprint "AABBCCDDEEFF11223344556677889900" -To "recipient@example.com" -FromAddress "sender@example.com" `
            -Subject "Manual Email" -EmailBody "Hello from Manual!"
        Uses the provided AppId, TenantId, and CertThumbprint directly (no vault) to obtain a token and send an email.
    .NOTES
        - This function requires the Microsoft.Graph, SecretManagement, SecretManagement.JustinGrote.CredMan,
            and MSAL.PS modules to be installed (handled automatically via Initialize-TkModuleEnv).
        - For the 'Vault' parameter set, the local vault secret must store JSON properties including AppId,
            TenantID, and CertThumbprint.
        - Refer to https://learn.microsoft.com/en-us/graph/outlook-send-mail for details on sending mail
            via Microsoft Graph.
#>

function Send-TkEmailAppMessage {
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium', DefaultParameterSetName = 'Vault')]
    param(
        # Use the vault-based approach (default)
        [Parameter(
            ParameterSetName = 'Vault',
            Mandatory = $true,
            HelpMessage = 'The name of the pre-created Email App from the vault.'
        )]
        [ValidateNotNullOrEmpty()]
        [string]
        $AppName,
        # Use the manual approach (no vault)
        [Parameter(
            ParameterSetName = 'Manual',
            Mandatory = $true,
            HelpMessage = 'Manually specify the App (Client) ID.'
        )]
        [ValidateNotNullOrEmpty()]
        [string]
        $AppId,
        [Parameter(
            ParameterSetName = 'Manual',
            Mandatory = $true,
            HelpMessage = 'Manually specify the Azure AD tenant (GUID or domain).'
        )]
        [ValidateNotNullOrEmpty()]
        [string]
        $TenantId,
        [Parameter(
            ParameterSetName = 'Manual',
            Mandatory = $true,
            HelpMessage = 'Manually specify the certificate thumbprint in Cert:\CurrentUser\My.'
        )]
        [ValidateNotNullOrEmpty()]
        [string]
        $CertThumbprint,
        # Common parameters for both parameter sets
        [Parameter(
            Mandatory = $true,
            ParameterSetName = 'Vault',
            HelpMessage = 'The email address of the recipient.'
        )]
        [Parameter(
            Mandatory = $true,
            ParameterSetName = 'Manual',
            HelpMessage = 'The email address of the recipient.'
        )]
        [ValidateNotNullOrEmpty()]
        [ValidatePattern('^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$')]
        [string]
        $To,
        [Parameter(
            Mandatory = $true,
            ParameterSetName = 'Vault',
            HelpMessage = 'The email address of the sender.'
        )]
        [Parameter(
            Mandatory = $true,
            ParameterSetName = 'Manual',
            HelpMessage = 'The email address of the sender.'
        )]
        [ValidateNotNullOrEmpty()]
        [ValidatePattern('^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$')]
        [string]
        $FromAddress,
        [Parameter(
            Mandatory = $true,
            ParameterSetName = 'Vault',
            HelpMessage = 'The subject line of the email.'
        )]
        [Parameter(
            Mandatory = $true,
            ParameterSetName = 'Manual',
            HelpMessage = 'The subject line of the email.'
        )]
        [ValidateNotNullOrEmpty()]
        [string]
        $Subject,
        [Parameter(
            Mandatory = $true,
            ParameterSetName = 'Vault',
            HelpMessage = 'The body text of the email.'
        )]
        [Parameter(
            Mandatory = $true,
            ParameterSetName = 'Manual',
            HelpMessage = 'The body text of the email.'
        )]
        [ValidateNotNullOrEmpty()]
        [string]
        $EmailBody,
        [Parameter(
            Mandatory = $false,
            ParameterSetName = 'Vault',
            HelpMessage = 'The path to the attachment file.'
        )]
        [Parameter(
            Mandatory = $false,
            ParameterSetName = 'Manual',
            HelpMessage = 'The path to the attachment file.'
        )]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({ Test-Path $_ -PathType 'Leaf' })]
        [string[]]
        $AttachmentPath,
        [Parameter(
            Mandatory = $false,
            ParameterSetName = 'Vault',
            HelpMessage = 'Vault name to retrieve the GraphEmailApp object.'
        )]
        [string]
        $VaultName = 'GraphEmailAppLocalStore'
    )
    begin {
        if (!($script:LogString)) {
            Write-AuditLog -Start
        }
        else {
            Write-AuditLog -BeginFunction
        }
        Write-AuditLog '###########################################################'
        # If manual parameter set:
        if ($PSCmdlet.ParameterSetName -eq 'Manual') {
            $cert = Get-ChildItem -Path Cert:\CurrentUser\My | Where-Object { $_.Thumbprint -eq $CertThumbprint } -ErrorAction Stop
            # TODO Confirm this object is not needed elsewhere
            $GraphEmailApp = @{
                AppId          = $AppId
                CertThumbprint = $CertThumbprint
                TenantID       = $TenantId
                CertExpires    = $cert.NotAfter
            }
            $Tenant = $TenantId
        }
        elseif ($PSCmdlet.ParameterSetName -eq 'Vault') {
            # Install and import the Microsoft.Graph module. Tested: 1.22.0
            $PublicMods = `
                'Microsoft.PowerShell.SecretManagement', 'SecretManagement.JustinGrote.CredMan'
            $PublicVers = `
                '1.1.2', '1.0.0', '4.37.0.0'
            $params1 = @{
                PublicModuleNames      = $PublicMods
                PublicRequiredVersions = $PublicVers
                Scope                  = 'CurrentUser'
            }
            Initialize-TkModuleEnv @params1
            # If a GraphEmailApp object was not passed in, attempt to retrieve it from the local machine
            if ($AppName) {
                try {
                    # Step 7:
                    # Define the application Name and Encrypted File Paths.
                    $Auth = Get-Secret -Name "CN=$AppName" -Vault $VaultName -AsPlainText -ErrorAction Stop
                    $authObj = $Auth | ConvertFrom-Json
                    $GraphEmailApp = $authObj
                    $AppId = $GraphEmailApp.AppId
                    $CertThumbprint = $GraphEmailApp.CertThumbprint
                    $Tenant = $GraphEmailApp.TenantID
                    $cert = Get-ChildItem -Path Cert:\CurrentUser\My | Where-Object { $_.Thumbprint -eq $CertThumbprint } -ErrorAction Stop
                }
                catch {
                    Write-Error $_.Exception.Message
                }
            } # End Region If
        }
        if (!$GraphEmailApp) {
            throw 'GraphEmailApp object not found. Please specify the GraphEmailApp object or provide the AppName and RedirectUri parameters.'
        } # End Region If
        # Instantiate the required variables for retrieving the token.
        # Retrieve the self-signed certificate from the CurrentUser's certificate store
        if (!($cert)) {
            throw "Certificate with thumbprint $CertThumbprint not found in CurrentUser's certificate store"
        } # End Region If
        Write-AuditLog 'The Certificate:'
        Write-AuditLog $CertThumbprint
        Write-AuditLog "will expire on $($GraphEmailApp.CertExpires)"
        Write-AuditLog -Message 'Retrieved Certificate with thumbprint:'
        Write-AuditLog "$CertThumbprint"
    } # End Region Begin
    Process {
        # https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-client-creds-grant-flow#second-case-access-token-request-with-a-certificate
        # Authenticate with Azure AD and obtain an access token for the Microsoft Graph API using the certificate
        $MSToken = Get-TkMsalToken `
            -ClientCertificate $Cert `
            -ClientId $AppId `
            -TenantId $Tenant `
            -ErrorAction Stop
        $authHeader = @{Authorization = "Bearer $MSToken" }
        # Set up the request URL
        $url = "https://graph.microsoft.com/v1.0/users/$($FromAddress)/sendMail"
        # Build the message body
        # Add a "from" field to the message object in $Message
        $FromField = @{
            emailAddress = @{
                address = "$($FromAddress)"
            }
        }
        $Message = @{
            message = @{
                subject      = "$Subject"
                body         = @{
                    contentType = 'text'
                    content     = "$EmailBody"
                }
                toRecipients = @(
                    @{
                        emailAddress = @{
                            address = "$To"
                        }
                    }
                )
                from         = $FromField
            }
        }
        if ($AttachmentPath) {
            Write-AuditLog -Message 'Attachments found. Processing...'
            $Message.message.attachments = @()
            foreach ($Path in $AttachmentPath) {
                $attachmentName = (Split-Path -Path $Path -Leaf)
                $attachmentBytes = [System.Convert]::ToBase64String([System.IO.File]::ReadAllBytes($Path))
                $attachment = @{
                    '@odata.type'  = '#microsoft.graph.fileAttachment'
                    'Name'         = $attachmentName
                    'ContentBytes' = $attachmentBytes
                }
                $Message.message.attachments += $attachment
            }
        }
        $jsonMessage = $message | ConvertTo-Json -Depth 4
        $body = $jsonMessage
        Write-AuditLog -Message 'Processed message body. Ready to send email.'
    }
    End {
        try {
            # Send the email message using the Invoke-RestMethod cmdlet
            Write-AuditLog 'Sending email via Microsoft Graph.'
            Write-AuditLog "To : $To"
            Write-AuditLog "From : $FromAddress"
            Write-AuditLog "Attachments To Send : $(($Message.message.attachments).Count)"
            $shouldProcessOperation = 'Send-TkEmailAppMessage'
            $shouldProcessTarget = "Sender: $FromAddress, Recipient: $To Attachments: $(($Message.message.attachments).Count)"
            if ($PSCmdlet.ShouldProcess($shouldProcessTarget, $shouldProcessOperation)) {
                [void](Invoke-RestMethod -Headers $authHeader -Uri $url -Body $body -Method POST -ContentType 'application/json' -ErrorAction Stop)
            }
            Write-AuditLog -EndFunction
        }
        catch {
            throw
        }
    } # End Region End
}
#EndRegion '.\Public\Send-TkEmailAppMessage.ps1' 314