PSServicePrincipal.psm1

$script:ModuleRoot = $PSScriptRoot
$script:ModuleVersion = (Import-PowerShellDataFile -Path "$($script:ModuleRoot)\PSServicePrincipal.psd1").ModuleVersion

# Detect whether at some level dotsourcing was enforced
$script:doDotSource = Get-PSFConfigValue -FullName PSServicePrincipal.Import.DoDotSource -Fallback $false
if ($PSServicePrincipal_dotsourcemodule) { $script:doDotSource = $true }

<#
Note on Resolve-Path:
All paths are sent through Resolve-Path/Resolve-PSFPath in order to convert them to the correct path separator.
This allows ignoring path separators throughout the import sequence, which could otherwise cause trouble depending on OS.
Resolve-Path can only be used for paths that already exist, Resolve-PSFPath can accept that the last leaf my not exist.
This is important when testing for paths.
#>


# Detect whether at some level loading individual module files, rather than the compiled module was enforced
$importIndividualFiles = Get-PSFConfigValue -FullName PSServicePrincipal.Import.IndividualFiles -Fallback $false
if ($PSServicePrincipal_importIndividualFiles) { $importIndividualFiles = $true }
if (Test-Path (Resolve-PSFPath -Path "$($script:ModuleRoot)\..\.git" -SingleItem -NewChild)) { $importIndividualFiles = $true }
if ("<was compiled>" -eq '<was not compiled>') { $importIndividualFiles = $true }
    
function Import-ModuleFile
{
    <#
        .SYNOPSIS
            Loads files into the module on module import.
         
        .DESCRIPTION
            This helper function is used during module initialization.
            It should always be dotsourced itself, in order to proper function.
             
            This provides a central location to react to files being imported, if later desired
         
        .PARAMETER Path
            The path to the file to load
         
        .EXAMPLE
            PS C:\> . Import-ModuleFile -File $function.FullName
     
            Imports the file stored in $function according to import policy
    #>

    [CmdletBinding()]
    Param (
        [string]
        $Path
    )
    
    $resolvedPath = $ExecutionContext.SessionState.Path.GetResolvedPSPathFromPSPath($Path).ProviderPath
    if ($doDotSource) { . $resolvedPath }
    else { $ExecutionContext.InvokeCommand.InvokeScript($false, ([scriptblock]::Create([io.file]::ReadAllText($resolvedPath))), $null, $null) }
}

#region Load individual files
if ($importIndividualFiles)
{
    # Execute Preimport actions
    . Import-ModuleFile -Path "$ModuleRoot\internal\scripts\preimport.ps1"
    
    # Import all internal functions
    foreach ($function in (Get-ChildItem "$ModuleRoot\internal\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore))
    {
        . Import-ModuleFile -Path $function.FullName
    }
    
    # Import all public functions
    foreach ($function in (Get-ChildItem "$ModuleRoot\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore))
    {
        . Import-ModuleFile -Path $function.FullName
    }
    
    # Execute Postimport actions
    . Import-ModuleFile -Path "$ModuleRoot\internal\scripts\postimport.ps1"
    
    # End it here, do not load compiled code below
    return
}
#endregion Load individual files

#region Load compiled code
<#
This file loads the strings documents from the respective language folders.
This allows localizing messages and errors.
Load psd1 language files for each language you wish to support.
Partial translations are acceptable - when missing a current language message,
it will fallback to English or another available language.
#>

Import-PSFLocalizedString -Path "$($script:ModuleRoot)\en-us\*.psd1" -Module 'PSServicePrincipal' -Language 'en-US'

Function Add-ExchangePermsToSPN.ps1 {
    <#
        .SYNOPSIS
            Applies the Manage.Exchange permissions to a registered application.
 
        .DESCRIPTION
            This function will apply the necessary application permissions needed for Exchange V2 CBA.
 
        .PARAMETER DisplayName
            Display name of the objects you are retrieving.
 
        .PARAMETER EnableException
            Disables user-friendly warnings and enables the throwing of exceptions. This is less user friendly, but allows catching exceptions in calling scripts.
 
        .EXAMPLE
            PS c:\> Add-ExchangePermsToSPN -DisplayName 'CompanySPN'
 
            Stamp the permissions on a registerd application by application id from the Azure active directory.
    #>


    [OutputType('System.String')]
    [CmdletBinding()]
    Param (
        [parameter(Position = 0, HelpMessage = "ApplicationID used to retrieve an application")]
        [ValidateNotNullOrEmpty()]
        [string]
        $DisplayName,

        [switch]
        $EnableException
    )

    process {
        try {
            Write-PSFMessage -Level Host -Message "Exchange.ManageAsApp roll applied to application {0}. To complete setup go to your application in the Azure portal and Grant Admin Consent." -StringValues $DisplayName
            $O365SvcPrincipal = Get-AzureADServicePrincipal -All $true | Where-object { $_.DisplayName -eq "Office 365 Exchange Online" }
            $reqExoAccess = New-Object -TypeName "Microsoft.Open.AzureAD.Model.RequiredResourceAccess"
            $reqExoAccess.ResourceAppId = $O365SvcPrincipal.AppId
            $delegatedPermissions = New-Object -TypeName "Microsoft.Open.AzureAD.Model.ResourceAccess" -ArgumentList "dc50a0fb-09a3-484d-be87-e023b12c6440", "Role" # Manage Exchange As Application
            $reqExoAccess.ResourceAccess = $delegatedPermissions
            $ADApplication = get-AzureADApplication -SearchString $DisplayName
            Set-AzureADApplication -ObjectId $ADApplication.ObjectId -RequiredResourceAccess $reqExoAccess
        }
        catch {
            Stop-PSFFunction -Message $_ -Cmdlet $PSCmdlet -ErrorRecord $_ -EnableException $EnableException
            return
        }
    }
}

Function Add-RoleToSPN {
    <#
        .SYNOPSIS
            Cmdlet for applying Role Assignments to service principal.
 
        .DESCRIPTION
            Applies the contributor role to a service principal object.
 
        .PARAMETER EnableException
            Disables user-friendly warnings and enables the throwing of exceptions. This is less user friendly, but allows catching exceptions in calling scripts.
 
        .PARAMETER SpnToProcess
            Service principal being passed into process.
 
        .EXAMPLE
            PS c:\> Add-RoleToSPN -SpnToProcess $newSPN
 
            ArrayList of SPN objects to be processed.
    #>


    [OutputType('System.String')]
    [CmdletBinding()]
    param(
        [object]
        $SpnToProcess,

        [switch]
        $EnableException
    )

    process {
        foreach ($spn in $SpnToProcess) {
            Invoke-PSFProtectedCommand -Action "Applying role assignment: Adding Contributor role to SPN" -Target $spn -ScriptBlock {
                Write-PSFMessage -Level Host -Message "Checking current Role Assignment. Waiting for AD Replication"
                $checkRole = Get-AzRoleAssignment -ObjectId $spn.id

                if (-NOT $checkRole) {
                    $newRole = New-AzRoleAssignment -ApplicationId $spn.ApplicationId -RoleDefinitionName "Contributor" -ErrorAction Stop
                    Write-PSFMessage -Level Host -Message "Appling Role Assignment: {0} to {1}" -StringValues $newRole.RoleDefinitionName, $newRole.DisplayName
                }
                else {
                    Write-PSFMessage -Level Host -Message "{0} already has this role assignment" -StringValues $spn.DisplayName
                }
            } -PSCmdlet $PSCmdlet -Continue -RetryCount 5 -RetryWait 5 -RetryErrorType Microsoft.Rest.Azure.CloudException -EnableException $EnableException
        }
    }
}

Function New-ServicePrincipal {
    <#
        .SYNOPSIS
            Cmdlet for creating a single object service principal objects
 
        .DESCRIPTION
            Create a single object Service Principal object based on application id.
 
        .PARAMETER ApplicationID
            Application id of the Azure application you are working on.
 
        .PARAMETER CertValue
            Certificate thumbprint being uploaded to the Azure registerd application.
 
        .PARAMETER CreateSPNWithPassword
            Used when a user supplied password is passed in.
 
        .PARAMETER DisplayName
            Display name of the object we are working on.
 
        .PARAMETER StartDate
            Certificate NotBefore time stamp.
 
        .PARAMETER EndDate
            Certificate NotAfter time stamp.
 
        .PARAMETER Cba
            Switch used to create a registered application, self-signed certificate, upload to the application, applies the correct application roll assignments.
 
        .PARAMETER EnableException
            Disables user-friendly warnings and enables the throwing of exceptions. This is less user friendly, but allows catching exceptions in calling scripts.
 
        .PARAMETER RegisteredApp
            Used to create an Azure registered application.
 
        .EXAMPLE
            PS c:\> New-ServicePrincipal -DisplayName 'CompanySPN'
 
            Create a new service principal with the display name of 'CompanySPN'.
 
        .EXAMPLE
            PS c:\> New-ServicePrincipal -RegisteredApp -ApplicationID 34a23ad2-dac4-4a41-bc3b-d12ddf90230e
 
            Create a new service principal with the application id '34a23ad2-dac4-4a41-bc3b-d12ddf90230e' from the newly created Azure registered application.
 
        .EXAMPLE
            PS c:\> New-ServicePrincipal -ApplicationID 34a23ad2-dac4-4a41-bc3b-d12ddf90230e
 
            Create a new service principal with the application id '34a23ad2-dac4-4a41-bc3b-d12ddf90230e'.
    #>


    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')]
    [OutputType('System.String')]
    [CmdletBinding()]
    param(
        [switch]
        $RegisteredApp,

        [ValidateNotNullOrEmpty()]
        [string]
        $ApplicationID,

        [ValidateNotNullOrEmpty()]
        [string]
        $CertValue,

        [switch]
        $CreateSPNWithPassword,

        [ValidateNotNullOrEmpty()]
        [string]
        $DisplayName,

        [ValidateNotNullOrEmpty()]
        [DateTime]
        $StartDate,

        [ValidateNotNullOrEmpty()]
        [DateTime]
        $EndDate,

        [switch]
        $Cba,

        [switch]
        $EnableException
    )

    process {
        try {
            # We can not create applications or service principals with special characters and spaces via Powershell but can in the azure portal
            $DisplayName -replace '[\W]', ''

            if ($RegisteredApp -and $ApplicationID) {
                # Registered Application needs ApplicationID
                Write-PSFMessage -Level Host -Message "Creating SPN with ApplicationID {0}" -StringValues $newSpn.ApplicationID
                $password = [guid]::NewGuid()
                $currentDate = Get-Date # Date here has to be different that cert date passed in
                $endCurrentDate = ($currentDate.AddYears(1))
                $securePassword = New-Object Microsoft.Azure.Commands.ActiveDirectory.PSADPasswordCredential -Property @{ StartDate = $currentDate; EndDate = $endCurrentDate; Password = $password }
                $newSpn = New-AzADServicePrincipal -ApplicationID $ApplicationID -PasswordCredential $securePassword -ErrorAction Stop
                $script:roleListToProcess.Add($newSpn)
                return
            }
        }
        catch {
            Stop-PSFFunction -Message $_ -Cmdlet $PSCmdlet -ErrorRecord $_ -EnableException $EnableException
            return
        }

        try {
            if ($DisplayName -and $CreateSPNWithPassword) {
                $password = Read-Host "Enter Password" -AsSecureString
                $securePassword = New-Object Microsoft.Azure.Commands.ActiveDirectory.PSADPasswordCredential -Property @{ StartDate = Get-Date; EndDate = Get-Date -Year 2024; Password = $password }
                $newSPN = New-AzADServicePrincipal -DisplayName $DisplayName -PasswordCredential $securePassword -ErrorAction Stop
                $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($password)
                $UnsecureSecret = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR)
                Write-PSFMessage -Level Host -Message "Creating SPN {0} - Secure Password {1}. WARNING: Backup this key!!! If you lose it you will need to reset the credentials for this SPN!" -StringValues $newSPN.DisplayName, $UnsecureSecret
                $script:roleListToProcess.Add($newSpn)
                return
            }
        }
        catch {
            Stop-PSFFunction -Message $_ -Cmdlet $PSCmdlet -ErrorRecord $_ -EnableException $EnableException
            return
        }

        try {
            if (($DisplayName) -and (-NOT $RegisteredApp)) {
                # Enterprise Application (Service Principal) needs display name because it creates the pair
                $newSpn = New-AzADServicePrincipal -DisplayName $DisplayName -ErrorAction Stop
                $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($newSpn.Secret)
                $UnsecureSecret = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR)
                Write-PSFMessage -Level Host -Message "Creating SPN DisplayName {0} and secret {1}. WARNING: Backup this key!!! If you lose it you will need to reset the credentials for this SPN!" -StringValues $DisplayName, $UnsecureSecret
                $script:roleListToProcess.Add($newSpn)
                return
            }
        }
        catch {
            Stop-PSFFunction -Message $_ -Cmdlet $PSCmdlet -ErrorRecord $_ -EnableException $EnableException
            return
        }

        try {
            # We enter here from new-selfsigned certificate so we need to accept the data stamps passed in from the certificate
            if ($DisplayName -and $RegisteredApp -and $Cba) {
                Write-PSFMessage -Level Host -Message "Creating registered application and SPN {0}. Certificate uploaded to Azure application" -StringValues $DisplayName
                $newSPN = New-AzADServicePrincipal -DisplayName $DisplayName -CertValue $CertValue -StartDate $StartDate -EndDate $EndDate -ErrorAction Stop
                $script:roleListToProcess.Add($newSpn)
                return
            }
        }
        catch {
            Stop-PSFFunction -Message $_ -Cmdlet $PSCmdlet -ErrorRecord $_ -EnableException $EnableException
            return
        }
    }
}

Function Connect-ToAzureInteractively {
    <#
        .SYNOPSIS
            Cmdlet for making an interactive connections to an Azure tenant and subscription.
 
        .DESCRIPTION
            Make an interactive connections to an Azure tenant and subscription. If interactive connection fails it will default to a manual connection.
 
        .PARAMETER EnableException
            Disables user-friendly warnings and enables the throwing of exceptions. This is less user friendly, but allows catching exceptions in calling scripts.
 
        .EXAMPLE
            PS c:\> Connect-ToAzureInteractively
 
            Make a connection to an Azure tenant.
    #>


    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')]
    [OutputType('System.String')]
    [CmdletBinding()]
    param(
        [switch]
        $EnableException
    )

    begin {
        # Can be modified by end user for interactive login to AzureAD and AzureAZ
        #$TenantID = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxx-xxxxxx'
        #$ApplicationID = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxx-xxxxxx'
        $CertThumbprint = (Get-ChildItem cert:\CurrentUser\My\ | Where-Object { $_.Subject -eq "CN=PSServicePrincipal" }).Thumbprint
    }

    process {
        if (-NOT $script:AdSessionFound) {
            if ($PSVersionTable.PSEdition -eq "Core") {
                Write-PSFMessage -Level Host -Message "At this time AzureAD PowerShell module does not work on PowerShell Core. Please use PowerShell version 5 or 6." -Once "PS Core Doesn't work with AzureAD"
                $script:runningOnCore = $true
            }
            elseif ($PSVersionTable.PSEdition -eq "Desktop") {
                try {
                    $script:AdSessionInfo = Connect-AzureAD -TenantId $TenantID -ApplicationId $ApplicationID -CertificateThumbprint $CertThumbprint -ErrorAction Stop
                    Write-PSFMessage -Level Host -Message "Connected to AzureAD with automatic logon"  -Once "Automatically connected to AzureAD"
                    $script:AdSessionFound = $true
                }
                catch {
                    Write-PSFMessage -Level Host -Message "Automatic logon to AzureAD failed. Defaulting to interactive connection" -Once "Interactive Logon Failed"
                    $script:AdSessionFound = $false

                    try {
                        $Credentials = Get-Credential -Message "Please enter your credentials for Connect-AzureAD"
                        $script:AdSessionInfo = Connect-AzureAD -Credential $Credentials -ErrorAction Stop
                        $script:AdSessionFound = $true
                        Write-PSFMessage -Level Host -Message "Connected to AzureAD successful" -Once "Interactive Logon Successful"
                    }
                    catch {
                        Stop-PSFFunction -Message $_ -Cmdlet $PSCmdlet -ErrorRecord $_ -EnableException $EnableException
                    }
                }
            }
        }

        if (-NOT $script:AzSessionFound) {
            try {
                $script:AzSessionInfo = Connect-AzAccount -ServicePrincipal -TenantId $TenantID -ApplicationId $ApplicationID -CertificateThumbprint $CertThumbprint -ErrorAction Stop
                Write-PSFMessage -Level Host -Message "Connected to AzureAZ with automatic logon" -Once "Automatically connected to AzureAZ"
                $script:AzSessionFound = $true
            }
            catch {
                Write-PSFMessage -Level Host -Message "Automatic logon to AzureAZ failed. Defaulting to interactive connection" -Once "Interactive Logon Failed"
                $script:AzSessionFound = $false

                try {
                    $Credentials = Get-Credential -Message "Please enter your credentials for Connect-AzAccount"
                    $script:AzSessionInfo = Connect-AzAccount -Credential $Credentials -ErrorAction Stop
                    $script:AzSessionFound = $true
                    Write-PSFMessage -Level Host -Message "Connected to AzureAZ successful" -Once "Interactive Logon Successful"
                }
                catch {
                    Stop-PSFFunction -Message $_ -Cmdlet $PSCmdlet -ErrorRecord $_ -EnableException $EnableException
                }
            }
        }
    }
}



Function Connect-ToCloudTenant {
    <#
        .SYNOPSIS
            Makes connections to an Azure tenant and subscription.
 
        .DESCRIPTION
            Connect to an Azure tenant and subscription.
 
        .PARAMETER Reconnect
            Used to force a new connection to an Azure tenant.
 
        .PARAMETER EnableException
            Disables user-friendly warnings and enables the throwing of exceptions. This is less user friendly, but allows catching exceptions in calling scripts.
 
        .EXAMPLE
            PS c:\> Connect-ToCloudTenant -Reconnect
 
            Makes a connection to an Azure tenant or reconnect to another specified tenant.
 
    #>


    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')]
    [OutputType('System.String')]
    [CmdletBinding()]
    param(
        [switch]
        $Reconnect,

        [switch]
        $EnableException
    )

    process {
        try {
            if ($Reconnect) {
                Write-PSFMessage -Level Host -Message "Forcing a reconnection to Azure" -Once "Forcing Connection"
                $Credentials = Get-Credential -Message "Please enter your credentials for Connect-AzureAD"
                $script:AdSessionInfo = Connect-AzureAD -Credential $Credentials -ErrorAction Stop
                $script:AdSessionFound = $true
                Write-PSFMessage -Level Host -Message "Connected to AzureAD successful as {0}" -StringValues $Credentials.UserName -Once "AzureAD Logon Successful"

                $Credentials = Get-Credential -Message "Please enter your credentials for Connect-AzAccount"
                $script:AzSessionInfo = Connect-AzAccount -Credential $Credentials -ErrorAction Stop
                $script:AzSessionFound = $true
                Write-PSFMessage -Level Host -Message "Connected to AzureAZ successful as {0}" -StringValues $Credentials.UserName -Once "AzureAZ Logon Successful"
                return
            }

            $script:AdSessionInfo = Get-AzureADCurrentSessionInfo -ErrorAction Stop
            Write-PSFMessage -Level Host -Message "AzureAD session found! Connected as {0} - Tenant {1} with Environment as {2}" -StringValues $script:AdSessionInfo.Account.Id, $script:AdSessionInfo.Tenant.Id, $script:AdSessionInfo.Environment.Name -Once "AD Connection Found"
            $script:AdSessionFound = $true
        }
        catch {
            Write-PSFMessage -Level Verbose -Message "No existing prior AzureAD connection." -Once "No Prior Connection"
            $script:AdSessionFound = $false
            Connect-ToAzureInteractively
        }

        try {
            Write-PSFMessage -Level Host -Message "Checking for an existing AzureAZ connection" -Once "No ADConnection"
            $script:AzSessionInfo = Get-AzContext

            if (-NOT $script:AzSessionInfo) {
                Write-PSFMessage -Level Host -Message "No existing prior AzureAZ connection." -Once "No AZ Connection"
                $script:AzSessionFound = $false
                Connect-ToAzureInteractively
            }
            else {
                Write-PSFMessage -Level Host -Message "AzureAZ session found! Connected to {0} as {1} - Tenant {2} - Environment as {3}" -StringValues $script:AzSessionInfo.Name, $script:AzSessionInfo.Account, $script:AzSessionInfo.Tenant, $script:AzSessionInfo.Environment.Name -Once "AZ Connection found"
                $script:AzSessionFound = $true
            }
        }
        catch {
            Stop-PSFFunction -Message $_ -Cmdlet $PSCmdlet -ErrorRecord $_ -EnableException $EnableException
        }
    }
}

Function Get-EnterpriseApplication {
    <#
        .SYNOPSIS
            Function for retrieving Azure active directory enterprise application.
 
        .DESCRIPTION
            Retrieve a single or group of enterprise applications from the Azure active directory.
 
        .PARAMETER ApplicationID
            ApplicationId of the object(s) being returned.
 
        .PARAMETER DisplayName
            DisplayName of the object(s) being returned.
 
        .PARAMETER ObjectID
            ObjectId of the object being(s) returned.
 
        .PARAMETER EnableException
            Disables user-friendly warnings and enables the throwing of exceptions. This is less user friendly, but allows catching exceptions in calling scripts.
 
        .EXAMPLE
            PS c:\> Get-EnterpriseApp -ApplicationID 34a23ad2-dac4-4a41-bc3b-d12ddf90230e
 
             Return an Azure active directory enterprise application by ApplicationID.
 
        .EXAMPLE
            PS c:\> Get-EnterpriseApp -DisplayName CompanySPN
 
            Return an Azure active directory enterprise application by DisplayName.
 
        .EXAMPLE
            PS c:\> Get-EnterpriseApp -ObjectID 94b26zd1-fah2-1a25-bsc5-7h3d6j3s5g3h
 
            Return an Azure active directory enterprise application by ObjectID.
    #>


    [OutputType('System.String')]
    [CmdletBinding()]
    Param (
        [parameter(HelpMessage = "DisplayName used to return enterprise application objects")]
        [string]
        $DisplayName,

        [parameter(HelpMessage = "ApplicationId used to return enterprise application objects")]
        [guid]
        $ApplicationId,

        [parameter(HelpMessage = "ObjectId used to return enterprise application objects")]
        [String]
        $ObjectId,

        [switch]
        $EnableException
    )

    process {
        $parameter = $PSBoundParameters | ConvertTo-PSFHashtable -Include DisplayName, ObjectId, ApplicationId
        if ((-NOT $script:AzSessionFound) -or (-NOT $script:AdSessionFound)) { Connect-ToAzureInteractively }

        Invoke-PSFProtectedCommand -Action "Enterprise application retrieved!" -Target $parameter.Values -ScriptBlock {
            Get-AzADApplication @parameter -ErrorAction Stop
        } -EnableException $EnableException -PSCmdlet $PSCmdlet
    }
}


Function Get-LogFolder {
    <#
        .SYNOPSIS
            Logs module information to output and debug logging folders.
 
        .DESCRIPTION
            Open the logging or debug logging directory.
 
        .PARAMETER LogFolder
            Tab complete allowing you to open the module debug logging folder which contains indepth debug logs for the logging provider.
 
        .EXAMPLE
            PS c:\> Get-LogFolder -LogFolder OutputLoggingFolder
 
            Open the output log folder.
 
        .EXAMPLE
            PS c:\> Get-LogFolder -LogFolder DebugLoggingFolder
 
            Open the debug output log folder.
    #>


    [CmdletBinding()]
    Param (
        [string]
        $LogFolder
    )

    process {
        switch ($LogFolder) {
            "OutputLoggingFolder" {
                $loggingFolder = Get-PSFConfigValue -FullName "PSServicePrincipal.Logging.PSServicePrincipal.LoggingFolderPath"
                Invoke-PSFProtectedCommand -Action "Invoking folder item" -Target $parameter.Values  -ScriptBlock {
                    Write-PSFMessage -Level Host -Message "Openning default logging foider {0}" -StringValues $loggingFolder
                    $loggingFolder | Invoke-Item
                } -EnableException $EnableException -PSCmdlet $PSCmdlet
            }

            "DebugLoggingFolder" {
                $debugFolder = Get-PSFConfigValue -FullName "PSFramework.Logging.FileSystem.LogPath"
                Invoke-PSFProtectedCommand -Action "Invoking folder item" -Target $parameter.Values -ScriptBlock {
                    Write-PSFMessage -Level Host -Message "Openning default logging foider {0}" -StringValues $debugFolder
                    $debugFolder | Invoke-Item
                } -EnableException $EnableException -PSCmdlet $PSCmdlet
            }
        }
    }
}

Function Get-RegisteredApplication {
    <#
        .SYNOPSIS
            Function for retrieving Azure active directory registered application.
 
        .DESCRIPTION
            Retrieve an Azure active directory registered application.
 
        .PARAMETER DisplayName
            DisplayName of the object(s) being returned.
 
        .PARAMETER ObjectID
            ObjectId of the object being(s) returned.
 
        .PARAMETER SearchString
            SearchString filter of the object being(s) returned.
 
        .PARAMETER EnableException
            Disables user-friendly warnings and enables the throwing of exceptions. This is less user friendly, but allows catching exceptions in calling scripts.
 
        .EXAMPLE
            PS c:\> Get-EnterpriseApp -DisplayName CompanySPN
 
            Return an Azure active directory registered application by DisplayName.
 
        .EXAMPLE
            PS c:\> Get-RegisteredApp -SearchString CompanyApp
 
            Return an Azure active directory registered application by SearchString.
 
        .EXAMPLE
            PS c:\> Get-EnterpriseApp -ObjectID 94b26zd1-fah2-1a25-bsc5-7h3d6j3s5g3h
 
            Return an Azure active directory registered application by ObjectID.
    #>


    [OutputType('System.String')]
    [CmdletBinding(DefaultParameterSetName = "DisplayNameSet")]
    Param (
        [parameter(HelpMessage = "Display name used to return registered application objects")]
        [string]
        $DisplayName,

        [parameter(HelpMessage = "ObjectID used to return registered application objects")]
        [ValidateNotNullOrEmpty()]
        [string]
        $ObjectId,

        [parameter(HelpMessage = "Search filter used to return registered application objects")]
        [ValidateNotNullOrEmpty()]
        [string]
        $SearchString,

        [switch]
        $EnableException
    )

    process {
        $parameter = $PSBoundParameters | ConvertTo-PSFHashtable -Include ObjectId, SearchString
        if ((-NOT $script:AzSessionFound) -or (-NOT $script:AdSessionFound)) { Connect-ToAzureInteractively }

        Invoke-PSFProtectedCommand -Action "Registered application retrieved!" -Target $parameter.Values -ScriptBlock {
            Get-AzureADApplication @parameter -ErrorAction Stop
        } -EnableException $EnableException -PSCmdlet $PSCmdlet
    }
}


Function Get-ServicePrincipalObject {
    <#
        .SYNOPSIS
            Filters active directory service principals by display name.
 
        .DESCRIPTION
            Retrieve a service principal from the Azure Active Directory by display name.
 
        .PARAMETER ApplicationID
            ApplicationId of the object(s) being returned.
 
        .PARAMETER DisplayName
            DisplayName of the object(s) being returned.
 
        .PARAMETER ObjectID
            ObjectId of the object(s) being returned.
 
        .PARAMETER SearchString
            SearchString filter used on object(s) being returned.
 
        .PARAMETER EnableException
            Disables user-friendly warnings and enables the throwing of exceptions. This is less user friendly, but allows catching exceptions in calling scripts.
 
        .EXAMPLE
            PS c:\> Get-ServicePrincipalObject -DisplayName CompanySPN
 
            Get an Azure active directory enterprise application by DisplayName.
 
        .EXAMPLE
            PS c:\> Get-ServicePrincipalObject -SearchString "Company"
 
            Get an Azure active directory enterprise application by using a filter.
 
        .EXAMPLE
            PS c:\> Get-ServicePrincipalObject -ApplicationID 34a23ad2-dac4-4a41-bc3b-d12ddf90230e
 
             Return an Azure active directory enterprise application by ApplicationID.
 
        .EXAMPLE
            PS c:\> Get-ServicePrincipalObject -ObjectID 94b26zd1-fah2-1a25-bsc5-7h3d6j3s5g3h
 
            Get an Azure active directory enterprise application by ObjectID.
    #>


    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')]
    [OutputType('System.String')]
    [CmdletBinding()]
    Param (
        [parameter(HelpMessage = "DisplayName used to return enterprise application objects")]
        [string]
        $DisplayName,

        [parameter(HelpMessage = "ApplicationId used to return enterprise application objects")]
        [guid]
        $ApplicationId,

        [parameter(HelpMessage = "ObjectId used to return enterprise application objects")]
        [String]
        $ObjectId,

        [parameter(HelpMessage = "SearchString used to return enterprise application objects")]
        [String]
        $SearchString,

        [switch]
        $EnableException
    )

    prcess {
        $parameter = $PSBoundParameters | ConvertTo-PSFHashtable -Include DisplayName, ObjectId, ApplicationId, SearchString
        if ((-NOT $script:AzSessionFound) -or (-NOT $script:AdSessionFound)) { Connect-ToAzureInteractively }
        Write-PSFMessage -Level Host "Retrieving SPN by Object(s)"

        try {
            $spnOutput = Get-AzADServicePrincipal @parameter | Select-PSFObject DisplayName, "ServicePrincipalNames as SPN", ApplicationId, "ID as ObjectID", ObjectType, Type
        }
        catch {
            Stop-PSFFunction -Message $_ -Cmdlet $PSCmdlet -ErrorRecord $_ -EnableException $EnableException
        }

        $count = 0
        foreach ($item in $spnOutput) {
            $count++
            [pscustomobject]@{
                PSTypeName    = 'PSServicePrincipal.Principal'
                ItemNumber    = $count
                DisplayName   = $item.DisplayName
                ApplicationID = $item.ApplicationID
                ObjectID      = $item.ObjectID
                ObjectType    = $item.ObjectType
                Type          = $item.Type
            }
        }
    }
}

Function New-SelfSignedCert
{
    <#
        .SYNOPSIS
            Cmdlet for creating a self-signed certificate.
 
        .DESCRIPTION
            This function will create a single self-signed certificate and place it in the local user and computer store locations. It will
            also export the .pfx and .cer files to a location of your choice.
 
        .PARAMETER CertificateName
            Name of the self-signed certificate.
 
        .PARAMETER DnsName
            DNS name on the self-signed certificate.
 
        .PARAMETER FilePath
            File path certificates are exported.
 
        .PARAMETER Password
            Secure password for the self-signed certificate.
 
        .PARAMETER RegisteredApp
            Switch used to create an Azure registered application.
 
        .PARAMETER SubjectAlternativeName
            SubjectAlternativeName on the self-signed certificate.
 
        .PARAMETER Cba
            Switch used to create a registered application, self-signed certificate, upload to the application, applies the correct application roll assignments.
 
        .PARAMETER EnableException
            Disables user-friendly warnings and enables the throwing of exceptions. This is less user friendly, but allows catching exceptions in calling scripts.
 
        .EXAMPLE
            PS c:\> New-SelfSignedCert -DnsName yourtenant.onmicrosoft.com -Subject "CN=PSServicePrincipal" -CertificateName MyNewCertificate -FilePath c:\temp\
 
            This will create a new self-signed certificate using a DNS and SubjectAlterntiveName, certificate name and export the certs to the c:\temp location
 
        .EXAMPLE
            PS c:\> New-SelfSignedCert -DnsName yourtenant.onmicrosoft.com -Subject "CN=PSServicePrincipal" -CertificateName MyNewCertificate -Password (ConvertTo-SecureString 'YourPassword' -AsPlainText -force)
 
            This will create a new self-signed certificate with a passed in secure password
 
        .NOTES
            You must run PowerShell as an administrator to run this function in order to create the certificate in the LocalMachine certificate store and to export to disk.
 
    #>


    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')]
    [OutputType('System.String')]
    [CmdletBinding(DefaultParameterSetName="SelfSignedCertSet")]
    param(
        [parameter(Position = 0, HelpMessage = "File path used to export the self-signed certificates")]
        [PSFValidateScript({Resolve-PSFPath $_ -Provider FileSystem -SingleItem}, ErrorMessage = "{0} - is not a legit folder" )]
        [string]
        $FilePath = (Get-PSFConfigValue -FullName "PSServicePrincipal.Cert.CertFolder"),

        [parameter(Mandatory = $True, ParameterSetName ="SelfSignedCertSet", HelpMessage = "Certificate name for self-signed certificate")]
        [ValidateNotNullOrEmpty()]
        [string]
        $CertificateName,

        [parameter(Mandatory = $True, Position = 1, ParameterSetName ="SelfSignedCertSet", HelpMessage = "DNS name for self-signed certificate")]
        [ValidateNotNullOrEmpty()]
        [string]
        $DnsName,

        [parameter(Mandatory = $True, Position = 2, ParameterSetName='SelfSignedCertSet', HelpMessage = "SubjectAlternativeName for self-signed certificate")]
        [ValidateNotNullOrEmpty()]
        [string]
        $SubjectAlternativeName,

        [SecureString]
        $Password = (Read-Host "Enter your self-signed certificate secure password" -AsSecureString),

        [parameter(HelpMessage = "Passed in from New-ServicePrincipalObject for registered application and cba cert upload bind")]
        [switch]
        $RegisteredApp,

        [parameter(HelpMessage = "Passed in from New-ServicePrincipalObject for auto cba cert upload to Azure application")]
        [switch]
        $Cba,

        [switch]
        $EnableException
    )

    process{
        try
        {
            $CertStore = 'cert:\CurrentUser\my\'
            $CurrentDate = Get-Date; $EndDate = $currentDate.AddYears(1)
            Write-PSFMessage -Level Host -Message "Creating new self-signed certficate with DnsName {0} in the following certificate store {1}" -StringValues $DnsName, $certStore
            $newSelfSignedCert = New-SelfSignedCertificate -certstorelocation $CertStore -Subject $SubjectAlternativeName -Dnsname $DnsName -NotBefore $CurrentDate -NotAfter $EndDate -Provider "Microsoft Enhanced RSA and AES Cryptographic Provider" -KeySpec KeyExchange -KeyExportPolicy Exportable -KeyUsage KeyEncipherment -KeyProtection None
            $script:certCounter ++
        }
        catch
        {
            Stop-PSFFunction -Message $_ -Cmdlet $PSCmdlet -ErrorRecord $_ -EnableException $EnableException
            return
        }

        try
        {
            if(-NOT (Test-Path -Path $FilePath))
            {
                Write-PSFMessage -Level Host -Message "File path {0} does not exist." -StringValues $FilePath
                $userChoice = Get-PSFUserChoice -Options "1) Create a new directory", "2) Exit" -Caption "User option menu" -Message "What operation do you want to perform?"

                switch($UserChoice)
                {
                    0 {if(New-Item -Path $FilePath -ItemType Directory){Write-PSFMessage -Level Host -Message "Directory {0} created!" -StringValues $FilePath}}
                    1 {return}
                }
            }
        }
        catch
        {
            Stop-PSFFunction -Message $_ -Cmdlet $PSCmdlet -ErrorRecord $_ -EnableException $EnableException
            return
        }

        try
        {
            # Export the pfx and cer files
            $PFXCert = Join-Path $FilePath "$CertificateName.pfx"
            $CERCert = Join-Path $FilePath "$CertificateName.cer"
            $path = $certStore + $newSelfSignedCert.thumbprint
            $null = Export-PfxCertificate -cert $path -FilePath $PFXCert -Password $Password
            [System.IO.File]::WriteAllBytes((Resolve-PSFPath $CERCert -Provider FileSystem -SingleItem -NewChild ), $newSelfSignedCert.GetRawCertData())
            Write-PSFMessage -Level Host -Message "Certificated created sucessfully! Exporting self-signed certificates {0} and {1} complete!" -StringValues $PFXCert, $CERCert
            $script:certExportedCounter = 2

            if($Cba -and $RegisteredApp)
            {
                $keyValue = [System.Convert]::ToBase64String($newSelfSignedCert.GetRawCertData())
                New-ServicePrincipal -DisplayName $DisplayName -CertValue $keyValue -StartDate $newSelfSignedCert.NotBefore -EndDate $newSelfSignedCert.NotAfter -Cba -RegisteredApp
            }
        }
        catch
        {
            Stop-PSFFunction -Message $_ -Cmdlet $PSCmdlet -ErrorRecord $_ -EnableException $EnableException
        }
    }
}

Function New-ServicePrincipalObject {
    <#
    .SYNOPSIS
        PowerShell module for creating, retrieving and removing Azure registered / enterprise applications and service principals.
 
    .DESCRIPTION
        This module will create creating, retrieving and remove Azure registered / enterprise applications and service principals objects
        that can be used for automation tasks. Enterprise applications created will also have a mirror service principal objects created. Azure
        registered applications will be linked to a new service principal which can then be use as an authentication mechanism for connecting applications
        and PowerShell session to an Office 365 and Azure tenant.
 
        The PSServicePrincipal logging provider is based on PSFramework: All messages are logged by default 'Documents\PowerShell Script Logs' on
        Windows and 'Documents/PowerShell Script Logs' on MacOS. There are two logging streams (output and debug). Both streams have respective
        folders for analysis: You can run Get-LogFolder -LogFolder [OutputLoggingFolder] and [DebugLoggingFolder] to access either logging stream directory.
 
        For more information on PSFramework please visit: https://psframework.org/
        PSFramework Logging: https://psframework.org/documentation/quickstart/psframework/logging.html
        PSFramework Configuration: https://psframework.org/documentation/quickstart/psframework/configuration.html
        PSGallery - PSFramework module - https://www.powershellgallery.com/packages/PSFramework/1.0.19
 
    .PARAMETER CreateSelfSignedCertificate
        Used when creating a single self-signed certificate to be used with registered and enterprise applications for certificate based connections.
 
    .PARAMETER CreateSingleObject
        Used when creating a single default enterprise application (service principal).
 
    .PARAMETER CreateBatchObjects
        Used when creating a batch of service principals from a text file.
 
    .PARAMETER CreateSPNWithAppID
        Used when creating a service principal and a registered Azure ApplicationID.
 
    .PARAMETER CreateSPNWithPassword
        Used when creating a service principal and a registered Azure application with a user supplied password.
 
    .PARAMETER CreateSPNsWithNameAndCert
        Used when creating a service principal and a registered Azure application using a display name and certificate.
 
    .PARAMETER Cba
        Used to create a registered application, self-signed certificate, upload to the application, applies the correct application roll assignments.
 
    .PARAMETER EnableException
        Disables user-friendly warnings and enables the throwing of exceptions. This is less user friendly, but allows catching exceptions in calling scripts.
 
    .PARAMETER RegisteredApp
        Switch used to create an Azure registered application.
 
    .PARAMETER Reconnect
        Used when forcing a new connection to an Azure tenant subscription.
 
    .PARAMETER ApplicationID
        Unique ApplicationId for a service principal in a tenant. Once created this property cannot be changed.
 
    .PARAMETER Certificate
        This parameter is the value of the "asymmetric" credential type. It represents the base 64 encoded certificate.
 
    .PARAMETER DisplayName
        DisplayName of the objects you are retrieving.
 
    .PARAMETER NameFile
        Name of the file that contains the list of service principals being passed in for creation.
 
    .PARAMETER ObjectID
        ObjectId for a service principal in a tenant. Once created this property cannot be changed.
 
    .EXAMPLE
        PS c:\> New-ServicePrincipalObject -Reconnect
 
        Force a reconnect to a specific Azure tenant.
 
    .EXAMPLE
        PS c:\> New-ServicePrincipalObject -CreateSelfSignedCertificate
 
        Create a basic self-signed certificate to be used with registered and enterprise applications for certificate-based connections.
 
    .EXAMPLE
        PS c:\> New-ServicePrincipalObject -DisplayName CompanySPN -CreateSingleObject
 
        Create a new service principal with a display name of 'CompanySPN' and password (an autogenerated GUID) and creates the service principal based on the application just created. The start date and end date are added to password credential.
 
    .EXAMPLE
        PS c:\> New-ServicePrincipalObject -DisplayName CompanySPN -CreateSingleObject -CreateSPNWithPassword
 
        Create a new Enterprise Application and service principal with a display name of 'CompanySPN' and a (user supplied password) and creates the service principal based on the application just created. The start date and end date are added to password credential.
 
     .EXAMPLE
        PS c:\> New-ServicePrincipalObject -DisplayName CompanyApp -CreateSingleObject -RegisteredApp -Cba
 
        Create a registered application with a display name of 'CompanyApp', a self-signed certificate which is uploaded to the application and applies the correct appRoll assignments.
 
    .EXAMPLE
        PS c:\> New-ServicePrincipalObject -ApplicationID 34a23ad2-dac4-4a41-bc3b-d12ddf90230e -CreateSingleObject -CreateSPNWithAppID
 
        Create a new Enterprise Application and service principal with the ApplicationID '34a23ad2-dac4-4a41-bc3b-d12ddf90230e'.
 
    .EXAMPLE
        PS c:\> New-ServicePrincipalObject -CreateBatchObjects -NameFile c:\temp\YourDataFile.txt
 
        Connect to an Azure tenant and creates a batch of enterprise applications and service principal objects from a file passed in.
 
    .EXAMPLE
        PS c:\> New-ServicePrincipalObject -DisplayName CompanySPN -CreateSPNsWithNameAndCert -CreateSingleObject -Certificate <public certificate as base64-encoded string>
 
        Create a new Enterprise Application and service principal with a display name of 'CompanySPN' and certifcate and creates the service principal based on the application just created. The end date is added to key credential.
 
    .NOTES
        When passing in the application ID it is the Azure ApplicationID from your registered application.
 
        WARNING: If you do not connect to an Azure tenant when you run Import-Module Az.Resources you will be logged in interactively to your default Azure subscription.
        After signing in, you will see information indicating which of your Azure subscriptions is active. If you have multiple Azure subscriptions in your account and
        want to select a different one, get your available subscriptions with Get-AzSubscription and use the Set-AzContext cmdlet with your subscription id.
 
        INFORMATION: The default parameter set uses default values for parameters if the user does not provide any.
        For more information on the default values used, please see the description for the given parameters below.
        This cmdlet can assign a role to the service principal with the Role 'Contributor'. Roles are applid at the end of the service principal creation.
        For more information on Azure RBAC roles please see: https://docs.microsoft.com/en-us/azure/role-based-access-control/built-in-roles
 
        Microsoft TechNet Documentation: https://docs.microsoft.com/en-us/powershell/module/az.resources/new-azadserviceprincipal?view=azps-3.8.0
    #>


    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')]
    [CmdletBinding(DefaultParameterSetName = 'Default')]
    [OutputType('System.Boolean')]
    [OutputType('System.String')]

    param(
        [parameter(ParameterSetName = "Reconnect")]
        [switch]
        $Reconnect,

        [parameter(Mandatory = $True, ParameterSetName = "DisplayName", HelpMessage = "Display name used to create or delete an SPN or application")]
        [ValidateNotNullOrEmpty()]
        [string]
        $DisplayName,

        [parameter(ParameterSetName = "DisplayName", HelpMessage = "Switch indicating to create a single service principal or application")]
        [switch]
        $CreateSingleObject,

        [parameter(ParameterSetName = "DisplayName", HelpMessage = "Switch for indicating to create self-signed certificate and upload it to the Azure application")]
        [switch]
        $Cba,

        [parameter(ParameterSetName = "DisplayName", HelpMessage = "Switch for indicating we want to create a Registered Azure application")]
        [switch]
        $RegisteredApp,

        [parameter(ParameterSetName = "CreateSelfSignedCertificate")]
        [switch]
        $CreateSelfSignedCertificate,

        [parameter(Mandatory = $True, ParameterSetName = 'CreateBatchObjects', HelpMessage = "Switch used to create batch objects")]
        [switch]
        $CreateBatchObjects,

        [parameter(Mandatory = $True, ParameterSetName = 'CreateBatchObjects', HelpMessage = "Name file used to create a batch of spn's")]
        [ValidateNotNullOrEmpty()]
        [string]
        $NameFile,

        [parameter(ParameterSetName = "ID", HelpMessage = "ApplicationID used to create or delete an SPN or application")]
        [ValidateNotNullOrEmpty()]
        [Guid]
        $ApplicationID,

        [parameter(ParameterSetName = "ID", HelpMessage = "Switch for creating spn with an ApplicationID")]
        [switch]
        $CreateSPNWithAppID,

        [parameter(ParameterSetName = "DisplayName", HelpMessage = "Switch for creating spn with a certificate and Base64 string")]
        [switch]
        $CreateSPNsWithNameAndCert,

        [parameter(ParameterSetName = "DisplayName", HelpMessage = "Switch for creating spn with a password")]
        [switch]
        $CreateSPNWithPassword,

        [parameter(HelpMessage = "Certificate parameter for a created spn")]
        [ValidateNotNullOrEmpty()]
        [string]
        $Certificate,

        [parameter(HelpMessage = "Enable exception logging")]
        [switch]
        $EnableException
    )

    process {

        # Adding admin check for Windows Version 2004 which needs admin access due to UAC
        if(-NOT (Test-PSFPowerShell -Elevated))
        {
            Write-PSFMessage -Level Host -Message "PSServicePrincipal security components need PowerShell to run as an administrator. Exiting" -StringValues $module
            return
        }

        $script:certCounter = 0
        $script:certExportedCounter = 0
        $script:runningOnCore = $false
        $script:AzSessionFound = $false
        $script:AdSessionFound = $false
        $script:AzSessionInfo = $null
        $script:AdSessionInfo = $null
        $script:roleListToProcess = New-Object -Type System.Collections.ArrayList
        $requiredModules = @("AzureAD", "Az.Accounts", "Az.Resources")

        Foreach ($module in $requiredModules) {
            if (-NOT (Get-Module -Name $module)) {
                Import-Module $module; Write-PSFMessage -Level Verbose -Message "Importing required modules {0}" -StringValues $module
            }
        }

        $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Include Reconnect
        Write-PSFMessage -Level Host -Message "Starting script run: {0}" -StringValues (Get-Date)

        if ($DumpCerts) {
            Write-PSFMessage -Level Host -Message "Opening CurenntUser and LocalMachine Certificate Stores"
            Get-ChildItem Cert:\CurrentUser\My | Sort-Object
            Get-ChildItem Cert:\LocalMachine\My | Sort-Object
            return
        }

        if ($CreateSelfSignedCertificate) {
            Invoke-PSFProtectedCommand -Action "Attempting to create self-signed certificate!" -Target $parameter.Values -ScriptBlock {
                New-SelfSignedCert @parameter -EnableException -ErrorAction Stop
            } -EnableException $EnableException -PSCmdlet $PSCmdlet
        }

        Invoke-PSFProtectedCommand -Action "Attempting to create self-signed certificate!" -Target $parameter.Values -ScriptBlock {
            Connect-ToCloudTenant @parameters -EnableException
            return
        } -EnableException $EnableException -PSCmdlet $PSCmdlet

        if ($CreateBatchObjects) {
            Write-PSFMessage -Level Host -Message "Testing access to {0}" -StringValues $NameFile

            if (-NOT (Test-Path -Path $NameFile)) {
                Stop-PSFFunction -Message "ERROR: File problem. Exiting" -EnableException $EnableException -Cmdlet $PSCmdlet -ErrorRecord $_
                return
            }
            else {
                Write-PSFMessage -Level Host -Message "{0} accessable. Reading file contents" -StringValues $NameFile
                $objectsToCreate = Get-Content $NameFile | Where-Object { $_ -ne "" }

                # Validate that we have data and if we dont we exit out
                if (0 -eq $objectsToCreate.Length) {
                    Stop-PSFFunction -Message "Error with imported content. Exiting" -EnableException $EnableException -Cmdlet $PSCmdlet -ErrorRecord $_
                    return
                }
            }

            # Check to make sure we have the list of objects to process
            if (-NOT $objectsToCreate) {
                Write-PSFMessage -Level Warning "ERROR: No list of objects found!"
                return
            }

            Write-PSFMessage -Level Host -Message "Object list DETECTED! Staring batch creation of SPN's"

            if ($RegisteredApp) {
                Write-PSFMessage -Level Host -Message "Creating batch registered Applications"
                if (-NOT $script:runningOnCore) {
                    foreach ($DisplayName in $objectsToCreate) {
                        try {
                            $newApp = New-AzureADApplication -DisplayName $DisplayName -ErrorAction SilentlyContinue -ErrorVariable ProcessError
                        }
                        catch {
                            Stop-PSFFunction -Message "ERROR creating batch spn's" -EnableException $EnableException -Cmdlet $PSCmdlet -ErrorRecord $_
                            return
                        }

                        if ($newApp) {
                            Write-PSFMessage -Level Host -Message "Registered Application created: DisplayName: {0} - ApplicationID {1}" -StringValues $newApp.DisplayName, $newApp.AppId
                            $script:roleListToProcess.Add($newApp)

                            # Since we only create an AzureADapplicaiaton we need to create the matching service principal
                            try {
                                New-ServicePrincipal -ApplicationID $newApp.AppID
                            }
                            catch {
                                Stop-PSFFunction -Message "ERROR creating batch spn's" -EnableException $EnableException -Cmdlet $PSCmdlet -ErrorRecord $_
                                return
                            }
                        }
                        elseif ($ProcessError) { Write-PSFMessage -Level Warning "WARNING: {0}" -StringValues $ProcessError.Exception.Message }
                    }

                    if ($script:roleListToProcess.Count -gt 0) { Add-RoleToSPN -spnToProcess $script:roleListToProcess }
                }
                else {
                    Write-PSFMessage -Level Host -Message "At this time AzureAD PowerShell module does not work on PowerShell Core. Please use PowerShell version 5 or 6 to create Registered Applications."
                }
            }
            else {
                Write-PSFMessage -Level Host -Message "Creating batch entperise service principal and applications"
                foreach ($spn in $objectsToCreate) { New-ServicePrincipal -DisplayName $spn }

                if ($roleListToProcess.Count -gt 0) { Add-RoleToSPN -spnToProcess $script:roleListToProcess }
            }
        }

        if ($CreateSingleObject) {
            try {
                if ($RegisteredApp) {
                    Write-PSFMessage -Level Host -Message "Creating registered applications"
                    if (-NOT $script:runningOnCore) {
                        if ($Cba) { New-SelfSignedCert -CertificateName $DisplayName -SubjectAlternativeName $DisplayName -Cba -RegisteredApp -EnableException }
                        else { $newApp = New-AzureADApplication -DisplayName $DisplayName -ErrorAction SilentlyContinue -ErrorVariable ProcessError }

                        if ($newApp) {
                            Write-PSFMessage -Level Host -Message "Registered Application created: DisplayName: {0} - ApplicationID {1}" -StringValues $newApp.DisplayName, $newApp.AppId
                            New-ServicePrincipal -ApplicationID $newApp.AppId -RegisteredApp # Since we only create an AzureADapplicaiaton we need to create the matching service principal
                        }
                        elseif ($ProcessError) { Write-PSFMessage -Level Warning "WARNING: $($ProcessError[0].Exception.Message)" }
                    }
                    else {
                        Write-PSFMessage -Level Host -Message "At this time AzureAD PowerShell module does not work on PowerShell Core. Please use PowerShell version 5 or 6 to create Registered Applications."
                    }
                }
                elseif ($DisplayName -and $CreateSPNWithPassword) { New-ServicePrincipal -DisplayName $DisplayName -CreateSPNWithPassword }
                elseif (($DisplayName) -and (-NOT $RegisteredApp) -and (-NOT $Cba) -and (-NOT $CreateSPNsWithNameAndCert)) { New-ServicePrincipal -DisplayName $DisplayName }

                if ($script:roleListToProcess.Count -gt 0) {
                    Add-RoleToSPN -spnToProcess $script:roleListToProcess
                    if ($Cba) { Add-ExchangePermsToSPN.ps1 -DisplayName $DisplayName }
                }
            }
            catch { Stop-PSFFunction -Message "ERROR: Creating a simple SPN failed" -EnableException $EnableException -Cmdlet $PSCmdlet -ErrorRecord $_ }
        }

        if ($CreateSPNWithAppID) {
            try {
                New-ServicePrincipal -ApplicationID $ApplicationID
            }
            catch {
                Stop-PSFFunction -Message "ERROR creating an spn by application id" -EnableException $EnableException -Cmdlet $PSCmdlet -ErrorRecord $_
                return
            }
        }

        if ($CreateSPNsWithNameAndCert) {
            try {
                if ((-NOT $Certificate)) {
                    Stop-PSFFunction -Message "ERROR: No service principal specified. Exiting" -EnableException $EnableException -Cmdlet $PSCmdlet
                    return
                }
                else {
                    Write-PSFMessage -Level Host -Message "Creating new SPN {0} and with auto generated certificate key" -StringValues $DisplayName
                    $endDate = Get-Date; $endDate = $currentDate.AddYears(1)
                    $newSPN = New-AzADServicePrincipal -DisplayName $DisplayName -CertValue $Certificate -EndDate $endDate -ErrorAction Stop
                    Add-RoleToSPN -spnToProcess $newSPN
                }
            }
            catch {
                Stop-PSFFunction -Message "ERROR: No certificate as base64-encoded string specified. Exiting" -EnableException $EnableException -Cmdlet $PSCmdlet -ErrorRecord $_
                return
            }
        }
    }

    end {
        Write-PSFMessage -Level Host -Message "End script run: {0}" -StringValues (Get-Date)
        Write-PSFMessage -Level Host -Message 'Log saved to: "{0}". Run Get-LogFolder to retrieve the output or debug logs.' -StringValues $script:loggingFolder
    }
}

Function Remove-ServicePrincipalObject {
    <#
        .SYNOPSIS
            Deletes an single Azure active directory application or service principal.
 
        .DESCRIPTION
            Delete an Application or Service Principal pair from the Azure Active Directory.
 
        .PARAMETER ApplicationID
            ApplicationID of the object you are deleting.
 
        .PARAMETER Confirm
            Stops processing before any changes are made to an object.
 
        .PARAMETER DeleteEnterpriseApp
            Used to delete an Azure enterprise application.
 
        .PARAMETER DeleteRegisteredApp
            Used to delete an Azure registered application.
 
        .PARAMETER DeleteSpn
            Used to delete a Service Principal.
 
        .PARAMETER DisplayName
            DisplayName of the objects you are deleting.
 
        .PARAMETER ServicePrincipalName
            ServicePrincipalName of the objects you are deleting.
 
        .PARAMETER EnableException
            Disables user-friendly warnings and enables the throwing of exceptions. This is less user friendly, but allows catching exceptions in calling scripts.
 
        .PARAMETER ObjectID
            ObjectID of the objects you are deleting.
 
        .PARAMETER WhatIf
            Only displays the objects that would be affected and what changes would be made to those objects (without the worry of modifying those objects)
 
        .EXAMPLE
            PS c:\> Remove-ServicePrincipalObject -DeleteRegisteredApp -DisplayName CompanySPN
 
            Delete a registered Azure application using the DisplayName 'CompanySPN'.
 
        .EXAMPLE
            PS c:\> Remove-ServicePrincipalObject -DeleteRegisteredApp -ApplicationID 34a23ad2-dac4-4a41-bc3b-d12ddf90230e
 
            Delete a registered Azure application using the ApplicationID.
 
        .EXAMPLE
            PS c:\> Remove-ServicePrincipalObject -DeleteRegisteredApp -ObjectID 34a23ad2-dac4-4a41-bc3b-d12ddf90230e
 
             Delete a registered Azure application using the ObjectID.
 
        .EXAMPLE
            PS c:\> Remove-ServicePrincipalObject -DeleteEnterpriseApp -DisplayName CompanySPN
 
            Delete an enterprise Azure application using the DisplayName 'CompanySPN'.
 
        .EXAMPLE
            PS c:\> Remove-ServicePrincipalObject -DeleteEnterpriseApp -ApplicationID 34a23ad2-dac4-4a41-bc3b-d12ddf90230e
 
            Delete an enterprise Azure application using the ApplicationID.
 
        .EXAMPLE
            PS c:\> Remove-ServicePrincipalObject -DeleteEnterpriseApp -ObjectID 34a23ad2-dac4-4a41-bc3b-d12ddf90230e
 
             Delete an enterprise Azure application using the ObjectID.
 
        .EXAMPLE
            PS c:\> Remove-ServicePrincipalObject -DeleteSpn -DisplayName CompanySPN
 
            Delete a service principal by the DisplayName.
 
        .EXAMPLE
            PS c:\> Remove-ServicePrincipalObject -ServicePrincipalName http://CompanySPN
 
            Delete a service principal by the ServicePrincipalName.
 
        .EXAMPLE
            PS c:\> Remove-ServicePrincipalObject -DeleteSpn -ApplicationID 34a23ad2-dac4-4a41-bc3b-d12ddf90230e
 
            Delete the service principal by the ApplicationID.
 
        .EXAMPLE
            PS c:\> Remove-ServicePrincipalObject -DeleteSpn -ObjectID 34a23ad2-dac4-4a41-bc3b-d12ddf90230e
 
            Delete the service principal by the ObjectID.
     #>


    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')]
    [CmdletBinding(SupportsShouldProcess = $True)]
    Param (
        [parameter(HelpMessage = "DisplayName used to delete an application object")]
        [ValidateNotNullOrEmpty()]
        [string]
        $DisplayName,

        [parameter(HelpMessage = "ApplicationID used to delete an application object")]
        [ValidateNotNullOrEmpty()]
        [string]
        $ApplicationId,

        [parameter(HelpMessage = "ObjectID used to delete an application object")]
        [ValidateNotNullOrEmpty()]
        [string]
        $ObjectId,

        [parameter(HelpMessage = "ServicePrincipalName used to delete an application object")]
        [ValidateNotNullOrEmpty()]
        [string]
        $ServicePrincipalName,

        [parameter(HelpMessage = "Switch used to delete an enterprise object")]
        [switch]
        $DeleteEnterpriseApp,

        [parameter(HelpMessage = "Switch used to delete an registered object")]
        [switch]
        $DeleteRegisteredApp,

        [parameter(HelpMessage = "Switch used to delete a service principal object")]
        [switch]
        $DeleteSpn,

        [switch]
        $EnableException
    )

    process {
        if (-NOT ($DeleteEnterpriseApp -or $DeleteRegisteredApp -or $DeleteSpn)) {
            Write-PSFMessage -Level Host -Message "You must past in one of the following switches -DeleteEnterpriseApp -DeleteRegisteredApp -or -DeleteSpn"
            return
        }

        $parameter = $PSBoundParameters | ConvertTo-PSFHashtable -include DisplayName, ApplicationId, ObjectId, ServicePrincipalName
        if ((-NOT $script:AzSessionFound) -or (-NOT $script:AdSessionFound)) { Connect-ToAzureInteractively }

        if ($DeleteEnterpriseApp) {
            Invoke-PSFProtectedCommand -Action "Enterprise application delete!" -Target $parameter.Values -ScriptBlock {
                Remove-AzADApplication @parameter -ErrorAction Stop
            } -EnableException $EnableException -PSCmdlet $PSCmdlet
        }

        if ($DeleteRegisteredApp) {
            Invoke-PSFProtectedCommand -Action "Registered application deleted!" -Target $parameter.Values -ScriptBlock {
                Remove-AzureADApplication @parameter -ErrorAction Stop

            } -EnableException $EnableException -PSCmdlet $PSCmdlet
        }

        if ($DeleteSpn) {
            if ($parameter.ContainsValue($ServicePrincipalName) -and (-NOT $ServicePrincipalName.Contains('http://'))) {
                $parameter.ServicePrincipalName = "http://$ServicePrincipalName"
            }

            Invoke-PSFProtectedCommand -Action "Service principal deleted!" -Target $parameter.Values -ScriptBlock {
                Remove-AzADServicePrincipal @parameter -ErrorAction Stop
            } -EnableException $EnableException -PSCmdlet $PSCmdlet

            $userChoice = Get-PSFUserChoice -Options "1) No", "2) Yes" -Caption "Delete matching Azure enterprise application" -Message "Would you like to delete the matching Azure enterprise application?"

            switch ($userChoice) {
                0 {
                    Write-PSFMessage -Level Host "No application deleted!"
                    return
                }

                1 {
                    # Remove-AzADApplication doesn't accept ServicePrincipal name so convert the parameter binding and set it to DisplayName
                    if ($parameter.ContainsValue($ServicePrincipalName) -and ($ServicePrincipalName.Contains('http://'))) {
                        $parameter.DisplayName = $ServicePrincipalName.substring(7)
                        $parameter.Remove('ServicePrincipalName')
                    }
                    elseif ($parameter.ContainsValue($ServicePrincipalName) -and (-NOT $ServicePrincipalName.Contains('http://'))) {
                        $parameter.DisplayName = $ServicePrincipalName
                        $parameter.Remove('ServicePrincipalName')
                    }

                    Invoke-PSFProtectedCommand -Action "Removing application!" -Target $parameter.Values -ScriptBlock {
                        Remove-AzADApplication @parameter -ErrorAction Stop
                    } -EnableException $EnableException -PSCmdlet $PSCmdlet
                }
            }
        }
    }
}

<#
This is an example configuration file
 
By default, it is enough to have a single one of them,
however if you have enough configuration settings to justify having multiple copies of it,
feel totally free to split them into multiple files.
#>


<#
# Example Configuration
Set-PSFConfig -Module 'PSServicePrincipal' -Name 'Example.Setting' -Value 10 -Initialize -Validation 'integer' -Handler { } -Description "Example configuration setting. Your module can then use the setting using 'Get-PSFConfigValue'"
#>


Set-PSFConfig -Module 'PSServicePrincipal' -Name 'Import.DoDotSource' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be dotsourced on import. By default, the files of this module are read as string value and invoked, which is faster but worse on debugging."
Set-PSFConfig -Module 'PSServicePrincipal' -Name 'Import.IndividualFiles' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be imported individually. During the module build, all module code is compiled into few files, which are imported instead by default. Loading the compiled versions is faster, using the individual files is easier for debugging and testing out adjustments."

# Local Certificate Storage
$CertFolder = Join-Path ([Environment]::GetFolderPath("MyDocuments")) "\Self-Signed Certificates"
if(-NOT (Test-Path -Path $CertFolder)) { New-Item -Path $CertFolder -ItemType Directory}

Set-PSFConfig -Module 'PSServicePrincipal' -Name 'Cert.CertFolder' -Value "$($CertFolder)" -Initialize -Validation string -Handler { } -Description "The default path where to save self-signed certificates. Supports some placeholders such as %Date% to allow for timestamp in the name. For full documentation on the supported wildcards, see the documentation on https://psframework.org"
Set-PSFConfig -Module 'PSServicePrincipal' -Name 'Import.IndividualFiles' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be imported individually. During the module build, all module code is compiled into few files, which are imported instead by default. Loading the compiled versions is faster, using the individual files is easier for debugging and testing out adjustments."


<#
Stored scriptblocks are available in [PsfValidateScript()] attributes.
This makes it easier to centrally provide the same scriptblock multiple times,
without having to maintain it in separate locations.
 
It also prevents lengthy validation scriptblocks from making your parameter block
hard to read.
 
Set-PSFScriptblock -Name 'PSServicePrincipal.ScriptBlockName' -Scriptblock {
     
}
#>


<#
# Example:
Register-PSFTeppScriptblock -Name "PSServicePrincipal.alcohol" -ScriptBlock { 'Beer','Mead','Whiskey','Wine','Vodka','Rum (3y)', 'Rum (5y)', 'Rum (7y)' }
#>



# Registers the Tab Complete Script Block with options to be passed in when you hit the TAB key
Register-PSFTeppScriptblock -Name "PSServicePrincipal.Logs" -ScriptBlock { 'OutputLoggingFolder', 'DebugLoggingFolder' }


<#
# Example:
Register-PSFTeppArgumentCompleter -Command Get-Alcohol -Parameter Type -Name PSServicePrincipal.alcohol
#>


Register-PSFTeppArgumentCompleter -Command Get-LogFolder -Parameter LogFolder -Name PSServicePrincipal.Logs


New-PSFLicense -Product 'PSServicePrincipal' -Manufacturer 'Dave Goldman' -ProductVersion $script:ModuleVersion -ProductType Module -Name MIT -Version "1.0.0.0" -Date (Get-Date "2020-04-28") -Text @"
Copyright (c) 2020 Dave Goldman
 
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
 
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
 
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"@


# Action that is performed on registration of the provider using Register-PSFLoggingProvider
$registrationEvent = {

}

#region Logging Execution
# Action that is performed when starting the logging script (or the very first time if enabled after launching the logging script)
$begin_event = {
    function Get-PSServicePrincipalPath
    {
        [CmdletBinding()]
        param (

        )

        $path = Get-PSFConfigValue -FullName 'PSServicePrincipal.Logging.PSServicePrincipal.FilePath'
        $logname = Get-PSFConfigValue -FullName 'PSServicePrincipal.Logging.PSServicePrincipal.LogName'

        $scriptBlock = {
            param (
                [string]
                $Match
            )

            $hash = @{
                '%date%'  = (Get-Date -Format 'yyyy-MM-dd')
                '%dayofweek%' = (Get-Date).DayOfWeek
                '%day%' = (Get-Date).Day
                '%hour%'   = (Get-Date).Hour
                '%minute%' = (Get-Date).Minute
                '%username%' = $env:USERNAME
                '%userdomain%' = $env:USERDOMAIN
                '%computername%' = $env:COMPUTERNAME
                '%processid%' = $PID
                '%logname%' = $logname
            }

            $hash.$Match
        }

        [regex]::Replace($path, '%day%|%computername%|%hour%|%processid%|%date%|%username%|%dayofweek%|%minute%|%userdomain%|%logname%', $scriptBlock)
    }

    function Write-PSServicePrincipalMessage
    {
        [CmdletBinding()]
        param (
            [Parameter(ValueFromPipeline = $true)]
            $Message,

            [bool]
            $IncludeHeader,

            [string]
            $FileType,

            [string]
            $Path,

            [string]
            $CsvDelimiter,

            [string[]]
            $Headers
        )

        $parent = Split-Path $Path
        if (-not (Test-Path $parent))
        {
            $null = New-Item $parent -ItemType Directory -Force
        }
        $fileExists = Test-Path $Path

        #region Type-Based Output
        switch ($FileType)
        {
            #region Csv
            "Csv"
            {
                if ((-not $fileExists) -and $IncludeHeader) { $Message | ConvertTo-Csv -NoTypeInformation -Delimiter $CsvDelimiter | Set-Content -Path $Path -Encoding UTF8 }
                else { $Message | ConvertTo-Csv -NoTypeInformation -Delimiter $CsvDelimiter | Select-Object -Skip 1 | Add-Content -Path $Path -Encoding UTF8 }
            }
            #endregion Csv
            #region Json
            "Json"
            {
                if ($fileExists) { Add-Content -Path $Path -Value "," -Encoding UTF8 }
                $Message | ConvertTo-Json | Add-Content -Path $Path -NoNewline -Encoding UTF8
            }
            #endregion Json
            #region XML
            "XML"
            {
                [xml]$xml = $message | ConvertTo-Xml -NoTypeInformation
                $xml.Objects.InnerXml | Add-Content -Path $Path -Encoding UTF8
            }
            #endregion XML
            #region Html
            "Html"
            {
                [xml]$xml = $message | ConvertTo-Html -Fragment

                if ((-not $fileExists) -and $IncludeHeader)
                {
                    $xml.table.tr[0].OuterXml | Add-Content -Path $Path -Encoding UTF8
                }

                $xml.table.tr[1].OuterXml | Add-Content -Path $Path -Encoding UTF8
            }
            #endregion Html
        }
        #endregion Type-Based Output
    }

    $PSServicePrincipal_includeheader = Get-PSFConfigValue -FullName 'PSServicePrincipal.Logging.PSServicePrincipal.IncludeHeader'
    $PSServicePrincipal_headers = Get-PSFConfigValue -FullName 'PSServicePrincipal.Logging.PSServicePrincipal.Headers'
    $PSServicePrincipal_filetype = Get-PSFConfigValue -FullName 'PSServicePrincipal.Logging.PSServicePrincipal.FileType'
    $PSServicePrincipal_CsvDelimiter = Get-PSFConfigValue -FullName 'PSServicePrincipal.Logging.PSServicePrincipal.CsvDelimiter'

    if ($PSServicePrincipal_headers -contains 'Tags')
    {
        $PSServicePrincipal_headers = $PSServicePrincipal_headers | ForEach-Object {
            switch ($_)
            {
                'Tags'
                {
                    @{
                        Name       = 'Tags'
                        Expression = { $_.Tags -join "," }
                    }
                }
                'Message'
                {
                    @{
                        Name       = 'Message'
                        Expression = { $_.LogMessage }
                    }
                }
                default { $_ }
            }
        }
    }

    $PSServicePrincipal_paramWriteLogFileMessage = @{
        IncludeHeader    = $PSServicePrincipal_includeheader
        FileType         = $PSServicePrincipal_filetype
        CsvDelimiter     = $PSServicePrincipal_CsvDelimiter
        Headers             = $PSServicePrincipal_headers
    }
}

# Action that is performed at the beginning of each logging cycle
$start_event = {
    $PSServicePrincipal_paramWriteLogFileMessage["Path"] = Get-PSServicePrincipalPath
}

# Action that is performed for each message item that is being logged
$message_Event = {
    Param (
        $Message
    )

    $Message | Select-Object $PSServicePrincipal_headers | Write-PSServicePrincipalMessage @PSServicePrincipal_paramWriteLogFileMessage
}

# Action that is performed for each error item that is being logged
$error_Event = {
    Param (
        $ErrorItem
    )


}

# Action that is performed at the end of each logging cycle
$end_event = {

}

# Action that is performed when stopping the logging script
$final_event = {

}
#endregion Logging Execution

#region Function Extension / Integration
# Script that generates the necessary dynamic parameter for Set-PSFLoggingProvider
$configurationParameters = {
    $configroot = "PSServicePrincipal.Logging.PSServicePrincipal"

    $configurations = Get-PSFConfig -FullName "$configroot.*"

    $RuntimeParamDic = New-Object  System.Management.Automation.RuntimeDefinedParameterDictionary

    foreach ($config in $configurations)
    {
        $ParamAttrib = New-Object System.Management.Automation.ParameterAttribute
        $ParamAttrib.ParameterSetName = '__AllParameterSets'
        $AttribColl = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
        $AttribColl.Add($ParamAttrib)
        $RuntimeParam = New-Object System.Management.Automation.RuntimeDefinedParameter(($config.FullName.Replace($configroot, "").Trim(".")), $config.Value.GetType(), $AttribColl)

        $RuntimeParamDic.Add(($config.FullName.Replace($configroot, "").Trim(".")), $RuntimeParam)
    }
    return $RuntimeParamDic
}

# Script that is executes when configuring the provider using Set-PSFLoggingProvider
$configurationScript = {
    $configroot = "PSServicePrincipal.Logging.PSServicePrincipal"

    $configurations = Get-PSFConfig -FullName "$configroot.*"

    foreach ($config in $configurations)
    {
        if ($PSBoundParameters.ContainsKey(($config.FullName.Replace($configroot, "").Trim("."))))
        {
            Set-PSFConfig -Module $config.Module -Name $config.Name -Value $PSBoundParameters[($config.FullName.Replace($configroot, "").Trim("."))]
        }
    }
}

# Script that returns a boolean value. "True" if all prerequisites are installed, "False" if installation is required
$isInstalledScript = {
    return $true
}

# Script that provides dynamic parameter for Install-PSFLoggingProvider
$installationParameters = {
    # None needed
}

# Script that performs the actual installation, based on the parameters (if any) specified in the $installationParameters script
$installationScript = {
    # Nothing to be done - if you need to install your filesystem, you probably have other issues you need to deal with first ;)
}
#endregion Function Extension / Integration

# Configuration settings to initialize
if($IsMacOS)
{
    $loggingFolder = Join-Path ([Environment]::GetFolderPath("MyDocuments")) "/Documents/PowerShell Script Logging"
    Set-PSFConfig -Module PSServicePrincipal -Name 'Logging.PSServicePrincipal.FilePath' -Value "$($loggingFolder)/%logname%-%date%.csv" -Initialize -Validation string -Handler { } -Description "The path to where the logfile is written. Supports some placeholders such as %Date% to allow for timestamp in the name. For full documentation on the supported wildcards, see the documentation on https://psframework.org"
    Set-PSFConfig -Module PSServicePrincipal -Name 'Logging.PSServicePrincipal.LoggingFolderPath' -Value "$($loggingFolder)" -Initialize -Validation string -Handler { } -Description "The path to where the logfile is written. Supports some placeholders such as %Date% to allow for timestamp in the name. For full documentation on the supported wildcards, see the documentation on https://psframework.org"
}
else
{
    $loggingFolder = Join-Path ([Environment]::GetFolderPath("MyDocuments")) "\PowerShell Script Logging"
    if(-NOT (Test-Path -Path $loggingFolder)) { New-Item -Path $loggingFolder -ItemType Directory}
    Set-PSFConfig -Module PSServicePrincipal -Name 'Logging.PSServicePrincipal.LoggingFolderPath' -Value "$($loggingFolder)" -Initialize -Validation string -Handler { } -Description "The path to where the logfile is written. Supports some placeholders such as %Date% to allow for timestamp in the name. For full documentation on the supported wildcards, see the documentation on https://psframework.org"
    Set-PSFConfig -Module PSServicePrincipal -Name 'Logging.PSServicePrincipal.FilePath' -Value "$($loggingFolder)\%logname%-%date%.csv" -Initialize -Validation string -Handler { } -Description "The path to where the logfile is written. Supports some placeholders such as %Date% to allow for timestamp in the name. For full documentation on the supported wildcards, see the documentation on https://psframework.org"
}
$configuration_Settings = {
    Set-PSFConfig -Module PSServicePrincipal -Name 'Logging.PSServicePrincipal.FilePath' -Value "$($script:loggingFolder)\%logname%-%date%.csv" -Initialize -Validation string -Handler { } -Description "The path to where the logfile is written. Supports some placeholders such as %Date% to allow for timestamp in the name. For full documentation on the supported wildcards, see the documentation on https://psframework.org"
    Set-PSFConfig -Module PSServicePrincipal -Name 'Logging.PSServicePrincipal.Logname' -Value "New-ServicePrincipalObject" -Initialize -Validation string -Handler { } -Description "A special string you can use as a placeholder in the logfile path (by using '%logname%' as placeholder)"
    Set-PSFConfig -Module PSServicePrincipal -Name 'Logging.PSServicePrincipal.IncludeHeader' -Value $true -Initialize -Validation bool -Handler { } -Description "Whether a written csv file will include headers"
    Set-PSFConfig -Module PSServicePrincipal -Name 'Logging.PSServicePrincipal.Headers' -Value @('Timestamp', 'Level', 'FunctionName', 'Message') -Initialize -Validation stringarray -Handler { } -Description "The properties to export, in the order to select them."
    Set-PSFConfig -Module PSServicePrincipal -Name 'Logging.PSServicePrincipal.FileType' -Value "CSV" -Initialize -Validation psframework.logfilefiletype -Handler { } -Description "In what format to write the logfile. Supported styles: CSV, XML, Html or Json. Html, XML and Json will be written as fragments."
    Set-PSFConfig -Module PSServicePrincipal -Name 'Logging.PSServicePrincipal.CsvDelimiter' -Value "," -Initialize -Validation string -Handler { } -Description "The delimiter to use when writing to csv."
    Set-PSFConfig -Module LoggingProvider -Name 'PSServicePrincipal.Enabled' -Value $true -Initialize -Validation "bool" -Handler { if ([PSFramework.Logging.ProviderHost]::Providers['PSServicePrincipal']) { [PSFramework.Logging.ProviderHost]::Providers['PSServicePrincipal'].Enabled = $args[0] } } -Description "Whether the logging provider should be enabled on registration"
    Set-PSFConfig -Module LoggingProvider -Name 'PSServicePrincipal.AutoInstall' -Value $false -Initialize -Validation "bool" -Handler { } -Description "Whether the logging provider should be installed on registration"
    Set-PSFConfig -Module LoggingProvider -Name 'PSServicePrincipal.InstallOptional' -Value $true -Initialize -Validation "bool" -Handler { } -Description "Whether installing the logging provider is mandatory, in order for it to be enabled"
    Set-PSFConfig -Module LoggingProvider -Name 'PSServicePrincipal.IncludeModules' -Value @('PSServicePrincipal') -Initialize -Validation "stringarray" -Handler { if ([PSFramework.Logging.ProviderHost]::Providers['PSServicePrincipal']) { [PSFramework.Logging.ProviderHost]::Providers['PSServicePrincipal'].IncludeModules = ($args[0] | Write-Output) } } -Description "Module whitelist. Only messages from listed modules will be logged"
    Set-PSFConfig -Module LoggingProvider -Name 'PSServicePrincipal.ExcludeModules' -Value @() -Initialize -Validation "stringarray" -Handler { if ([PSFramework.Logging.ProviderHost]::Providers['PSServicePrincipal']) { [PSFramework.Logging.ProviderHost]::Providers['PSServicePrincipal'].ExcludeModules = ($args[0] | Write-Output) } } -Description "Module blacklist. Messages from listed modules will not be logged"
    Set-PSFConfig -Module LoggingProvider -Name 'PSServicePrincipal.IncludeTags' -Value @() -Initialize -Validation "stringarray" -Handler { if ([PSFramework.Logging.ProviderHost]::Providers['PSServicePrincipal']) { [PSFramework.Logging.ProviderHost]::Providers['PSServicePrincipal'].IncludeTags = ($args[0] | Write-Output) } } -Description "Tag whitelist. Only messages with these tags will be logged"
    Set-PSFConfig -Module LoggingProvider -Name 'PSServicePrincipal.ExcludeTags' -Value @() -Initialize -Validation "stringarray" -Handler { if ([PSFramework.Logging.ProviderHost]::Providers['PSServicePrincipal']) { [PSFramework.Logging.ProviderHost]::Providers['PSServicePrincipal'].ExcludeTags = ($args[0] | Write-Output) } } -Description "Tag blacklist. Messages with these tags will not be logged"
}

# This is needed to register the PSServicePrincipal logging provider
Register-PSFLoggingProvider -Name "PSServicePrincipal" -RegistrationEvent $registrationEvent -BeginEvent $begin_event -StartEvent $start_event -MessageEvent $message_Event -ErrorEvent $error_Event -EndEvent $end_event -FinalEvent $final_event -ConfigurationParameters $configurationParameters -ConfigurationScript $configurationScript -IsInstalledScript $isInstalledScript -InstallationScript $installationScript -InstallationParameters $installationParameters -ConfigurationSettings $configuration_Settings
#endregion Load compiled code