WinEventLogCustomization.psm1

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

# Detect whether at some level dotsourcing was enforced
$script:doDotSource = Get-PSFConfigValue -FullName WinEventLogCustomization.Import.DoDotSource -Fallback $false
if ($WinEventLogCustomization_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 WinEventLogCustomization.Import.IndividualFiles -Fallback $false
if ($WinEventLogCustomization_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
    foreach ($path in (& "$ModuleRoot\internal\scripts\preimport.ps1")) {
        . Import-ModuleFile -Path $path
    }

    # 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
    $functions = (Get-ChildItem "$ModuleRoot\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore)
    $function = $functions[6]
    foreach ($function in $functions) {
        . Import-ModuleFile -Path $function.FullName
    }

    # Execute Postimport actions
    foreach ($path in (& "$ModuleRoot\internal\scripts\postimport.ps1")) {
        . Import-ModuleFile -Path $path
    }

    # 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 'WinEventLogCustomization' -Language 'en-US'

function Import-Excel {
    <#
    .Synopsis
        Import-Excel
 
    .DESCRIPTION
        Imports data from Excel
 
    .PARAMETER ExcelPackage
        The Excel package imported from a file
 
    .PARAMETER WorksheetName
        Name of the sheet within the Excel package
 
    .PARAMETER StartRow
        Number of the row where import starts
 
    .PARAMETER EndRow
        Number of the row where import ends
 
    .PARAMETER StartColumn
        Number of the column where import starts
 
    .PARAMETER EndColumn
        Number of the column where import ends
 
    .EXAMPLE
        PS C:\> Import-Excel -ExcelPackage $excelPackage -WorksheetName "Sheet1" -StartRow 1 -EndRow 10 -StartColumn 1 -EndColumn 5
 
        Imports data from $excelPackage
 
    .NOTES
        Derived function from PSModule "ImportExcel" by Douglas Finke
 
        Due to the fact, that I don't need the whole function of the module and want to avoid module dependencies,
        I've adopted and cut the function down to my own need the WinEventLogCustomization module.
 
    .LINK
        https://github.com/dfinke/ImportExcel
 
    #>

    [CmdLetBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [OfficeOpenXml.ExcelPackage]
        $ExcelPackage,

        [ValidateNotNullOrEmpty()]
        [String]
        $WorksheetName,

        [Int]
        $StartRow = 1,

        [Int]
        $EndRow,

        [Int]
        $StartColumn = 1,

        [Int]
        $EndColumn
    )

    begin {
        # Helper function
        function Get-PropertyNames {
            <#
            .SYNOPSIS
                Create objects containing the column number and the column name for each of the different header types.
            #>

            [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = "Name would be incorrect, and command is not exported")]
            param(
                [Parameter(Mandatory = $true)]
                $Sheet,

                [Parameter(Mandatory = $true)]
                [Int[]]
                $Columns,

                [Parameter(Mandatory = $true)]
                [Int]
                $StartRow
            )

            if ($StartRow -lt 1) {
                Stop-PSFFunction -Message 'The top row can never be less than 1 when we need to retrieve headers from the worksheet.' -EnableException $true
                return
            }

            try {
                foreach ($column in $Columns) {
                    $Sheet.Cells[$StartRow, $column] | Where-Object { -not [string]::IsNullOrEmpty($_.Value) } | Select-Object @{N = 'Column'; E = { $column } }, Value
                }
            } catch {
                Stop-PSFFunction -Message "Failed creating property names: $_" -EnableException $true
                return
            }
        }
    }

    process {
        try {
            $sheet = $ExcelPackage.Workbook.Worksheets[$WorksheetName]
            if (-not $sheet) {
                Stop-PSFFunction -Message "Worksheet '$WorksheetName' not found" -EnableException
                return
            }


            #region Get rows and columns
            if (-not $EndRow ) { $EndRow = $sheet.Dimension.End.Row }
            if (-not $EndColumn) { $EndColumn = $sheet.Dimension.End.Column }

            $Columns = $StartColumn .. $EndColumn

            if ($StartColumn -gt $EndColumn) {
                Write-PSFMessage -Level Warning -Message "Selecting columns $StartColumn to $EndColumn might give odd results."
            }

            $rows = (1 + $StartRow) .. $EndRow
            if ($StartRow -eq 1 -and $EndRow -eq 1) {
                $rows = 0
            }
            #endregion Get rows and columns


            #region Create property names
            if ((-not $Columns) -or (-not ($PropertyNames = Get-PropertyNames -Sheet $sheet -Columns $Columns -StartRow $StartRow))) {
                Write-PSFMessage -Level Error -Message "No column headers found on top row '$StartRow'."
                return
            }

            if ($Duplicates = $PropertyNames | Group-Object Value | Where-Object Count -GE 2) {
                Stop-PSFFunction -Message "Duplicate column headers found on row '$StartRow' in columns '$($Duplicates.Group.Column)'. Column headers must be unique." -EnableException $true
                return
            }
            #endregion


            if (-not $rows) {
                Write-PSFMessage -Level Warning -Message "Worksheet '$WorksheetName' contains no data in the rows after top row '$StartRow'"
            } else {
                # Create one object per row
                foreach ($row in $rows) {
                    Write-PSFMessage -Level Debug -Message "Import row '$row'"

                    $NewRow = [Ordered]@{}
                    foreach ($propertyName in $PropertyNames) {
                        $NewRow[$propertyName.Value] = $sheet.Cells[$row, $propertyName.Column].Value
                    }

                    $NewRow
                }

            }
        } catch {
            Stop-PSFFunction -Message "Failed importing the Excel workbook. $_" -EnableException $true
            return
        }
    }

    end {
    }
}


function Get-WELCEventChannel {
    <#
    .Synopsis
        Get-WELCEventChannel
 
    .DESCRIPTION
        Query Windows Eventlog Channel(s) and their provider information.
 
    .PARAMETER ComputerName
        The computer(s) to connect to.
        Supports PSSession objects also.
 
    .PARAMETER Session
        A PSSession object for remote connection to another machine
 
    .PARAMETER Credential
        The credentials to use on remote calls
 
    .PARAMETER ChannelFullName
        The name of the EventChannel to query
 
        Default is every channel
 
    .EXAMPLE
        PS C:\> Get-WELCEventChannel
 
        Display all available subscription
 
    .EXAMPLE
        PS C:\> Get-WELCEventChannel -ChannelFullName MyChannel
 
        Display Channel "MyChannel"
 
    .EXAMPLE
        PS C:\> Get-WELCEventChannel -ChannelFullName MyChannel -ComputerName SRV01
 
        Display Channel "MyChannel" from remote computer "SRV01".
 
    .EXAMPLE
        PS C:\> Get-WELCEventChannel -ChannelFullName MyChannel -Sesion $PSSession
 
        Display Channel "MyChannel" from all connections within the $PSSession variable
 
        Assuming $PSSession variable is created something like this:
        $PSSession = New-PSSession -ComputerName SRV01
 
    .NOTES
        Author: Andreas Bellstedt
 
    .LINK
        https://github.com/AndiBellstedt/WinEventLogCustomization
 
    #>

    [CmdletBinding(
        DefaultParameterSetName = 'ComputerName',
        PositionalBinding = $true,
        ConfirmImpact = 'low'
    )]
    [OutputType([WELC.EventLogChannel])]
    Param(
        [Parameter(
            ValueFromPipeline = $true,
            Position = 0
        )]
        [Alias("Name", "ChannelName", "LogName")]
        [ValidateNotNullOrEmpty()]
        [String[]]
        $ChannelFullName = "*",

        [Parameter(
            ParameterSetName = "ComputerName",
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            Position = 1
        )]
        [Alias("Host", "Hostname", "Computer", "DNSHostName")]
        [PSFComputer[]]
        $ComputerName = $env:COMPUTERNAME,

        [Parameter(
            ParameterSetName = "Session",
            Position = 1
        )]
        [System.Management.Automation.Runspaces.PSSession[]]
        $Session,

        [Parameter( ParameterSetName = "ComputerName" )]
        [PSCredential]
        $Credential
    )

    begin {
        # If session parameter is used -> transfer it to ComputerName,
        # The class "PSFComputer" from PSFramework can handle it. This simplifies the handling in the further process block
        if ($Session) { $ComputerName = $Session.ComputerName }

        $channelFullNameBound = Test-PSFParameterBinding -ParameterName ChannelFullName
        $computerBound = Test-PSFParameterBinding -ParameterName ComputerName
    }

    process {
        #region parameterset workarround
        Write-PSFMessage -Level Debug -Message "ParameterNameSet: $($PsCmdlet.ParameterSetName)"

        # Workarround parameter binding behaviour of powershell in combination with ComputerName Piping
        if (-not ($channelFullNameBound -or $computerBound) -and $ComputerName.InputObject -and $PSCmdlet.ParameterSetName -ne "Session") {
            if ($ComputerName.InputObject -is [string]) { $ComputerName = $env:ComputerName } else { $ChannelFullName = "*" }
        }
        #endregion parameterset workarround

        #region Processing Channels
        foreach ($computer in $ComputerName) {
            $winEventProviders = @()
            $errorChannel = @()

            foreach ($channel in $ChannelFullName) {

                $ErrorReturn = $null
                $paramInvokeCmd = [ordered]@{
                    "ComputerName" = $computer
                    "ErrorAction"  = "Stop"
                    ErrorVariable  = "ErrorReturn"
                    "ArgumentList" = $channel
                }
                if ($PSCmdlet.ParameterSetName -eq "Session") { $paramInvokeCmd['ComputerName'] = $Session }
                if ($Credential) { $paramInvokeCmd.Add("Credential", $Credential) }

                Write-PSFMessage -Level Verbose -Message "Query EventLog channel '$($channel)' on computer '$($computer)'" -Target $computer
                try {
                    $winEventChannels = Invoke-PSFCommand @paramInvokeCmd -ScriptBlock { Get-WinEvent -ListLog $args[0] -ErrorAction Stop }
                } catch {
                    Stop-PSFFunction -Message "Unable to query EventLog channel '$($channel)' on computer '$($computer)'. ErrorMessage: $($ErrorReturn.Exception.Message | Select-Object -Unique)" -Target $computer -ErrorRecord $_
                    continue
                }

                [array]$providerNames = $winEventChannels.ProviderNames | Select-Object -Unique
                Write-PSFMessage -Level Verbose -Message "Query $($providerNames.count) provider from EventLog channel '$($winEventChannels.LogName)'" -Target $computer

                $errorMessages = @()
                $providerNames = $providerNames | Where-Object { $_ -notin $winEventProviders.name -and $_ -notin $errorChannel }
                $winEventProviders += foreach ($providerName in $providerNames) {
                    $ErrorReturn = $null
                    $paramInvokeCmd['ArgumentList'] = $providerName
                    try {
                        Invoke-PSFCommand @paramInvokeCmd -ScriptBlock {
                            Get-WinEvent -ListProvider $args -ErrorAction SilentlyContinue -ErrorVariable ErrorOccured
                            if ($ErrorOccured) { Write-Error -Message ([string]::Join(" " , ($ErrorOccured.Exception.Message | Select-Object -Unique))) -ErrorAction Stop }
                        } -Verbose:$false
                    } catch {
                        Write-PSFMessage -Level Debug -Message "Unable to query provider '$($providerName)' from computer '$($computer)'. ErrorMessage: $($ErrorReturn.Exception | Select-Object -ExpandProperty Message -Unique)" -Target $computer -ErrorRecord $_
                        $errorChannel += $providerName
                        $errorMessages += $ErrorReturn.Exception[-1].Message
                    }
                }

                if ($errorMessages) {
                    Write-PSFMessage -Level Error -Message "Error query provider ($([string]::Join(", ", $providerNames))) from EventLog Channel '$($winEventChannels.LogName)' on computer '$($computer)'. ErrorMessage: $([string]::join(" ", $errorMessages))" -Target $computer
                }

                Write-PSFMessage -Level Verbose -Message "Output $(([Array]$winEventChannels).count) EventLog channel$(if(([Array]$winEventChannels).count -gt 1){"s"})" -Target $computer
                # Output result
                foreach ($winEventChannel in $winEventChannels) {

                    $output = [WELC.EventLogChannel]@{
                        "PSComputerName" = $computer
                        "WinEventLog"    = $winEventChannel
                        "Provider"       = ( $winEventChannel.ProviderNames | ForEach-Object { $_name = $_; $winEventProviders | Where-Object ProviderName -like $_name } )
                    }

                    $output
                }
            }
        }
        #endregion Processing Events

    }

    end {
    }
}


function Import-WELCChannelDefinition {
    <#
    .Synopsis
        Import-WELCChannelDefinition
 
    .DESCRIPTION
        Import definition data for creating custom Windows EventLog Channels from a Excel file
        The Excel file acts as a definition database and provide easy handling and definition for custom eventlog channels and there structure
 
        Additionally in the excel file, there is the possibility to manage XPath-Queries for Windows Event Forwading queries
 
    .PARAMETER Path
        The Excel file or a folder with Excel files to import
 
    .PARAMETER Sheet
        The Name of the sheet within the Excel file
 
    .PARAMETER Table
        The table containing the definition data within the sheet of the Excel file
 
    .PARAMETER FileExtension
        A list of file extensions indicating Excel files
        Only needed/used if a folder is specified as a Path
 
    .PARAMETER Recursive
        The specified path will be parsed recursivly
        Only needed/used if a folder is specified as a Path
 
    .PARAMETER OutputChannelDefinition
        If specified the function will output a WELC.ChannelDefinition object, instead of WELC.TemplateRecord data
 
    .PARAMETER OutputChannelConfig
        If specified the function will output a WELC.ChannelDefinition object, instead of WELC.TemplateRecord data
 
 
    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
 
    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
 
    .EXAMPLE
        PS C:\> Import-WELCChannelDefinition -Path C:\WELC\MyFile.xls
 
        Import the excel file 'C:\WELC\MyFile.xls' with the default expected parametersettings
        (Excel file has to contain a Sheet 'CustomEventLogChannels' and a table 'T_Channel')
 
    .EXAMPLE
        PS C:\> Import-WELCChannelDefinition -Path C:\WELC
 
        Import all excel files in path 'C:\WELC' with the default expected parametersettings
        (sheet and table settings like in first example. Files have to have an extension with ".xlsx", ".xlsm", ".xls")
 
    .EXAMPLE
        PS C:\> Import-WELCChannelDefinition -Path C:\WELC -Recursive -FileExtension "xlsx", "xlsm", "xls"
 
        Import all excel files in path 'C:\WELC' and in all subfolders with the specified extensions "xlsx", "xlsm", "xls"
        (sheet and table settings like in first example)
 
    .EXAMPLE
        PS C:\> Import-WELCChannelDefinition -Path C:\WELC\MyFile.xls -Sheet "CustomEventLogChannels" -Table "T_Channel"
 
        Import the excel file 'C:\WELC\MyFile.xls' with the explicit parameter settings on sheet and table
 
    .NOTES
        Author: Andreas Bellstedt
 
    .LINK
        https://github.com/AndiBellstedt/WinEventLogCustomization
    #>

    [CmdLetBinding(
        DefaultParameterSetName = "OutputTemplateRecord",
        SupportsShouldProcess = $true,
        PositionalBinding = $true,
        ConfirmImpact = 'Low'
    )]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseOutputTypeCorrectly', '', Justification = 'Intentional')]
    [OutputType("WELC.ChannelDefinition", "WELC.ChannelConfig", "WELC.TemplateRecord", "PSObject")]
    param(
        [parameter(
            Mandatory = $true,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            Position = 0
        )]
        [Alias("FullName", "FilePath", "Folder", "File")]
        [string[]]
        $Path,

        [ValidateNotNullOrEmpty()]
        [Alias("ExcelSheet", "SheetName")]
        [string]
        $Sheet = "CustomEventLogChannels",

        [ValidateNotNullOrEmpty()]
        [Alias("ExcelTable", "TableName")]
        [string]
        $Table = "T_Channel",

        [string[]]
        $FileExtension = @("xlsx", "xlsm", "xls"),

        [switch]
        $Recursive,

        [Parameter(ParameterSetName = "OutputChannelDefinition" )]
        [switch]
        $OutputChannelDefinition,

        [Parameter(ParameterSetName = "OutputChannelConfig" )]
        [switch]
        $OutputChannelConfig
    )

    begin {
        # ensure correct format for specified extensions
        if ($FileExtension) {
            $FileExtension = foreach ($item in $FileExtension) {
                $item = $item.Trim(".")
                ".$($item)"
            }
        }
    }

    process {
        Write-PSFMessage -Level Debug -Message "ParameterNameSet: $($PsCmdlet.ParameterSetName)"

        # working trough the specified path(s)
        foreach ($pathItem in $Path) {

            # File and folder validity tests
            if (Test-Path -Path $pathItem -PathType Leaf) {
                Write-PSFMessage -Level Verbose -Message "Found file '$($pathItem)' as a valid file in path"
                $files = $pathItem | Resolve-Path | Get-ChildItem | Select-Object -ExpandProperty FullName
            } elseif (Test-Path -Path $pathItem -PathType Container) {
                Write-PSFMessage -Level Verbose -Message "Getting files in path '$($pathItem)'"
                $param = @{
                    Path   = $pathItem
                    "File" = $true
                }
                if ($Recursive) { $param["Recursive"] = $true }
                $files = Get-ChildItem @param | Where-Object Extension -in $FileExtension | Select-Object -ExpandProperty FullName
                Write-PSFMessage -Level Verbose -Message "Found $($files.count) file$(if($files.count -gt 1){"s"}) in path "
            } elseif (-not (Test-Path  -Path $pathItem -PathType Any -IsValid)) {
                Write-PSFMessage -Level Error -Message "'$pathItem' is not a valid path or file."
                continue
            } else {
                Write-PSFMessage -Level Error -Message "Unable to open '$($pathItem)'"
                continue
            }

            # Working trough the actual found file(s)
            foreach ($file in $files) {
                Write-PSFMessage -Level Verbose -Message "Open file '$($file)' as Excel file"

                # Open the file
                #$excelDocument = Get-ExcelDocument -Path $file
                $excelDocument = New-Object -TypeName OfficeOpenXml.ExcelPackage -ArgumentList $file
                if (-not $excelDocument) { continue }

                # Select the specified sheet
                $excelSheet = $excelDocument.Workbook.Worksheets | Where-Object name -like $Sheet
                if (-not $excelSheet) {
                    Write-PSFMessage -Level Error -Message "Excel file '$($file.split("\")[-1])' contains no sheet '$($Sheet)'"
                    continue
                }

                # Select the specified table
                $excelTable = $excelSheet.Tables | Where-Object name -like $Table
                if (-not $table) {
                    Write-PSFMessage -Level Error -Message "Unable to find table '$($Table)' in sheet '$($file.split("\")[-1])' "
                    continue
                }

                # Prepare importing the table as powershell object
                $param = @{
                    ExcelPackage  = $excelDocument
                    WorksheetName = $excelSheet.name
                    StartRow      = $excelTable.Address.Start.Row
                    StartColumn   = $excelTable.Address.Start.Column
                    EndRow        = $excelTable.Address.End.Row
                    EndColumn     = $excelTable.Address.End.Column
                }

                if ($pscmdlet.ShouldProcess("table '$($Table)' in sheet '$($Sheet)' from file '$($file)'", "Import")) {
                    Write-PSFMessage -Level Debug -Message "Import Excel file"
                    # Import and filter data from excel table into powershell
                    $tableData = Import-Excel @param
                    $data = $tableData | Where-Object LogFullName
                    Write-PSFMessage -Level Verbose -Message "Found $(([array]$data).Count) usable records in $($tableData.Count) records from table '$($Table)' in worksheet '$($Sheet)'"

                    # Output result
                    foreach ($item in $data) {
                        switch ($pscmdlet.ParameterSetName) {
                            "OutputChannelDefinition" {
                                if ($OutputChannelDefinition) {
                                    $output = [WELC.ChannelDefinition]@{
                                        ChannelName    = $item.ChannelName
                                        ChannelSymbol  = $item.ChannelSymbol
                                        ProviderName   = $item.ProviderName
                                        ProviderSymbol = $item.ProviderSymbol
                                    }
                                    $output
                                }
                            }
                            "OutputChannelConfig" {
                                if ($OutputChannelConfig) {
                                    $output = [WELC.ChannelConfig]@{
                                        ChannelName     = $item.ChannelName
                                        LogFullName     = $item.LogFullName
                                        LogMode         = $item.LogMode
                                        Enabled         = [bool]::Parse($item.ChannelEnabled)
                                        MaxEventLogSize = $item.MaxEventLogSize / 1
                                    }
                                    $output
                                }
                            }
                            "OutputTemplateRecord" {
                                $item.psobject.TypeNames.Insert(0, "WELC.TemplateRecord")
                                $item
                            }

                            Default {
                                Stop-PSFFunction -Message "Unhandeled ParameterSetName. Developers mistake." -EnableException $true
                                throw
                            }
                        }
                    }
                }

                # Data/variable cleanup
                Write-PSFMessage -Level Debug -Message "Close Excel file and cleanup variables"
                $excelSheet.Dispose()
                $excelDocument.Dispose()
                Remove-Variable excelDocument, excelSheet, excelTable, tableData, data, param, item -Force -Confirm:$false -ErrorAction:Ignore
            }
            Remove-Variable file, files -Force -Confirm:$false -ErrorAction Ignore
        }
        Remove-Variable pathItem -Force -Confirm:$false -ErrorAction Ignore
    }

    end {
    }
}

function Move-WELCEventChannelManifest {
    <#
    .SYNOPSIS
        Move-WELCEventChannelManifest
 
    .DESCRIPTION
        Move a manifest with the compiled DLL file from a source to destination directory
 
        The manifest has to be rewritten to fit the destination path otherwise,
        there will be errors when registering/usering it
 
    .PARAMETER Path
        The path to the manifest file
 
        You can specify the fullname of the manifest-file (.man), or just the directory
 
        In case of a directory, all the manifest files in the directory will be processed
 
    .PARAMETER DestinationPath
        The path where to store the manifest and DLL file
 
    .PARAMETER Prepare
        The rewrite of the manifest will be done, but the files will not be moved
 
    .PARAMETER CopyMode
        Copy the files from the source to the destination, instead of moving them
 
    .PARAMETER PassThru
        The moved files will be parsed to the pipeline for further processing.
 
    .PARAMETER WhatIf
            If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
 
    .PARAMETER Confirm
            If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
 
    .NOTES
        Author: Andreas Bellstedt
 
    .LINK
        https://github.com/AndiBellstedt/WinEventLogCustomization
 
    .EXAMPLE
        PS C:\> Move-WELCEventChannelManifest -Path C:\CustomDLLPath\MyChannel.man -DestinationPath $env:WinDir\System32
 
        The manifest and its DLL file will be copied to the system32 directory of the current windows installation
 
    .EXAMPLE
        PS C:\> Move-WELCEventChannelManifest -Path C:\CustomDLLPath -DestinationPath $env:WinDir\System32
 
        All manifest files will copied over to the system32 directory.
 
    .EXAMPLE
        PS C:\> Move-WELCEventChannelManifest -Path C:\CustomDLLPath\MyChannel.dll -DestinationPath $env:WinDir\System32 -Prepare
 
        The manifest will rewritten to the destination folder, but the actual file will not be moved
    #>

    [CmdletBinding(
        SupportsShouldProcess = $true,
        PositionalBinding = $true,
        ConfirmImpact = 'Medium'
    )]
    param (
        [Parameter(
            Mandatory = $true,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            Position = 0
        )]
        [ValidateNotNullOrEmpty()]
        [Alias("File", "FileName", "FullName")]
        [String[]]
        $Path,

        [Parameter(
            Mandatory = $true,
            Position = 1
        )]
        [String]
        $DestinationPath,

        [switch]
        $Prepare,

        [switch]
        $CopyMode,

        [switch]
        $PassThru
    )

    begin {
        $DestinationPath = $DestinationPath.TrimEnd("\")
        $DestinationPath = $DestinationPath | Resolve-Path | Get-Item | Select-Object -ExpandProperty FullName
    }

    process {
        $files = @()
        foreach ($pathItem in $Path) {
            # File and folder validity tests
            if (Test-Path -Path $pathItem -PathType Leaf) {
                Write-PSFMessage -Level Verbose -Message "Found file '$($pathItem)' as a valid file in path" -Target $env:COMPUTERNAME
                $files = $pathItem | Resolve-Path | Get-ChildItem | Select-Object -ExpandProperty FullName
            } elseif (Test-Path -Path $pathItem -PathType Container) {
                Write-PSFMessage -Level Verbose -Message "Getting files in path '$($pathItem)'" -Target $env:COMPUTERNAME
                $files = Get-ChildItem -Path $pathItem -File -Filter "*.man" | Select-Object -ExpandProperty FullName
                Write-PSFMessage -Level Verbose -Message "Found $($files.count) file$(if($files.count -gt 1){"s"}) in path" -Target $env:COMPUTERNAME
                if (-not $files) { Write-PSFMessage -Level Warning -Message "No manifest files found in path '$($pathItem)'" -Target $env:COMPUTERNAME }
            } elseif (-not (Test-Path  -Path $pathItem -PathType Any -IsValid)) {
                Write-PSFMessage -Level Error -Message "'$pathItem' is not a valid path or file." -Target $env:COMPUTERNAME
                continue
            } else {
                Write-PSFMessage -Level Error -Message "unable to open '$($pathItem)'" -Target $env:COMPUTERNAME
                continue
            }

            foreach ($file in $files) {

                # open XML file
                Write-PSFMessage -Level Verbose -Message "Opening XML manifest file '$($file)' to gather DLL information"
                $xmlfile = New-Object XML
                $xmlfile.Load($file)

                if (
                    $xmlfile.instrumentationManifest.schemaLocation -eq "http://schemas.microsoft.com/win/2004/08/events eventman.xsd" -and
                    $xmlfile.instrumentationManifest.xmlns -eq "http://schemas.microsoft.com/win/2004/08/events" -and
                    $xmlfile.instrumentationManifest.win -eq "http://manifests.microsoft.com/win/2004/08/windows/events" -and
                    $xmlfile.instrumentationManifest.xsi -eq "http://www.w3.org/2001/XMLSchema-instance" -and
                    $xmlfile.instrumentationManifest.xs -eq "http://www.w3.org/2001/XMLSchema" -and
                    $xmlfile.instrumentationManifest.trace -eq "http://schemas.microsoft.com/win/2004/08/events/trace"
                ) {
                    # Gather files and rewrite XML
                    $manifestFolder = Split-Path -Parent $file

                    Write-PSFMessage -Level Debug -Message "Gather path of resourceFileName DLL"
                    $resourceFileNameFullName = $xmlfile.instrumentationManifest.instrumentation.events.provider.resourceFileName
                    $resourceFileNamePath = Split-Path -Path $resourceFileNameFullName
                    $resourceFileNameFile = Split-Path -Path $resourceFileNameFullName -Leaf
                    if ($DestinationPath -like $resourceFileNamePath) {
                        Write-PSFMessage -Level Significant -Message "Source and destination path of ressource file '$resourceFileNameFullName' are the same. Nothing to do"
                    } else {
                        $destResourceFileName = "$($DestinationPath)\$($resourceFileNameFile)"
                        $xmlfile.instrumentationManifest.instrumentation.events.provider.resourceFileName = $destResourceFileName
                        Write-PSFMessage -Level Verbose -Message "Rewrite path of messageFileName DLL from '$($resourceFileNameFullName)' to '$($destResourceFileName)'"

                        if (Test-Path -Path $destResourceFileName -PathType Leaf) {
                            # DLL is already present in destination directory
                            $resourceFileNameFullName = $destResourceFileName
                        } elseif (Test-Path -Path "$($manifestFolder)\$($resourceFileNameFile)" -PathType Leaf) {
                            # DLL path in XML is wrong, but DLL is next to manifest file
                            $resourceFileNameFullName = "$($manifestFolder)\$($resourceFileNameFile)"
                        } elseif (Test-Path -Path $resourceFileNameFullName -PathType Leaf) {
                            # nothing to do, DLL is in path from xml file, but has to be mnoved somewhere different
                        } else {
                            Stop-PSFFunction -Message "Ressource file '$($resourceFileNameFile)' not found. Searched in folders: '$($resourceFileNamePath)', '$($manifestFolder)', '$($DestinationPath)'" -EnableException $true
                        }
                    }

                    Write-PSFMessage -Level Debug -Message "Gather path of messageFileName DLL"
                    $messageFileNameFullName = $xmlfile.instrumentationManifest.instrumentation.events.provider.messageFileName
                    $messageFileNamePath = Split-Path -Path $messageFileNameFullName
                    $messageFileNameFile = Split-Path -Path $messageFileNameFullName -Leaf
                    if ($DestinationPath -like $messageFileNamePath) {
                        Write-PSFMessage -Level Verbose -Message "Source and destination path of message file '$($messageFileNameFullName)' are the same. Nothing to do"
                    } else {
                        $destMessageFileName = "$($DestinationPath)\$($messageFileNameFile)"
                        $xmlfile.instrumentationManifest.instrumentation.events.provider.messageFileName = $destMessageFileName
                        Write-PSFMessage -Level Verbose -Message "Rewrite path of messageFileName DLL from '$($messageFileNameFullName)' to '$($destMessageFileName)'"

                        if (Test-Path -Path $destMessageFileName -PathType Leaf) {
                            # DLL is already present in destination directory
                            $messageFileNameFullName = $destMessageFileName
                        } elseif (Test-Path -Path "$($manifestFolder)\$($messageFileNameFile)" -PathType Leaf) {
                            # DLL path in XML is wrong, but DLL is next to manifest file
                            $messageFileNameFullName = "$($manifestFolder)\$($messageFileNameFile)"
                        } elseif (Test-Path -Path $messageFileNameFullName -PathType Leaf) {
                            # nothing to do, DLL is in path from xml file, but has to be mnoved somewhere different
                        } else {
                            Stop-PSFFunction -Message "Message file '$($messageFileNameFile)' not found. Searched in folders: '$($messageFileNamePath)', '$($manifestFolder)', '$($DestinationPath)'" -EnableException $true
                        }
                    }

                    Write-PSFMessage -Level Debug -Message "Gather path of parameterFileName DLL"
                    $parameterFileNameFullName = $xmlfile.instrumentationManifest.instrumentation.events.provider.parameterFileName
                    $parameterFileNamePath = Split-Path -Path $parameterFileNameFullName
                    $parameterFileNameFile = Split-Path -Path $parameterFileNameFullName -Leaf
                    if ($DestinationPath -like $parameterFileNamePath) {
                        Write-PSFMessage -Level Verbose -Message "Source and destination path of parameter file '$($parameterFileNameFullName)' are the same. Nothing to do"
                    } else {
                        $destParameterFileName = "$($DestinationPath)\$($parameterFileNameFile)"
                        $xmlfile.instrumentationManifest.instrumentation.events.provider.parameterFileName = $destParameterFileName
                        Write-PSFMessage -Level Verbose -Message "Rewrite path of parameterFileName DLL from '$($parameterFileNameFullName)' to '$($destParameterFileName)'"

                        if (Test-Path -Path $destParameterFileName -PathType Leaf) {
                            # DLL is already present in destination directory
                            $parameterFileNameFullName = $destParameterFileName
                        } elseif (Test-Path -Path "$($manifestFolder)\$($parameterFileNameFile)" -PathType Leaf) {
                            # DLL path in XML is wrong, but DLL is next to manifest file
                            $parameterFileNameFullName = "$($manifestFolder)\$($parameterFileNameFile)"
                        } elseif (Test-Path -Path $parameterFileNameFullName -PathType Leaf) {
                            # nothing to do, DLL is in path from xml file, but has to be mnoved somewhere different
                        } else {
                            Stop-PSFFunction -Message "Parameter file '$($parameterFileNameFile)' not found. Searched in folders: '$($parameterFileNamePath)', '$($manifestFolder)', '$($DestinationPath)'" -EnableException $true
                        }
                    }
                } else {
                    Stop-PSFFunction -Message "$($file) is not a actual manifest file" -EnableException $true
                }

                if ($pscmdlet.ShouldProcess("file '$($file)' with directory '$($DestinationPath)'", "Set")) {
                    $xmlfile.Save($file)
                    Write-PSFMessage -Level Verbose -Message "Save file '$($file)' in directory '$($DestinationPath)'"
                }

                if (-not $Prepare -or $CopyMode) {
                    Write-PSFMessage -Level Verbose -Message "Copy/Move manifest and DLL files into directory '$($DestinationPath)'"

                    if ($pscmdlet.ShouldProcess("File manifest '$($file)' to '$($DestinationPath)'$(if($CopyMode){"in CopyMode"})", "Move")) {
                        if ($CopyMode) {
                            Write-PSFMessage -Level Debug -Message "Copy manifest file"
                            $destfile = Copy-Item -Path $file -Destination $DestinationPath -Force -PassThru
                        } else {
                            Write-PSFMessage -Level Debug -Message "Move manifest file"
                            $destfile = Move-Item -Path $file -Destination $DestinationPath -Force -PassThru
                        }
                    }

                    if ($pscmdlet.ShouldProcess("Dll file '$($resourceFileNameFullName)' to '$($DestinationPath)'$(if($CopyMode){"in CopyMode"})", "Move")) {
                        if ($CopyMode) {
                            Write-PSFMessage -Level Debug -Message "Copy resourceFileName dll file"
                            Copy-Item -Path $resourceFileNameFullName -Destination $DestinationPath -Force
                        } else {
                            Write-PSFMessage -Level Debug -Message "Move resourceFileName dll file"
                            Move-Item -Path $resourceFileNameFullName -Destination $DestinationPath -Force
                        }
                    }

                    if ($messageFileNameFullName -notlike $resourceFileNameFullName) {
                        if ($pscmdlet.ShouldProcess("File message dll file '$($messageFileNameFullName)' to '$($DestinationPath)'$(if($CopyMode){"in CopyMode"})", "Move")) {
                            if ($CopyMode) {
                                Write-PSFMessage -Level Debug -Message "Copy messageFileName dll file"
                                Copy-Item -Path $messageFileNameFullName -Destination $DestinationPath -Force
                            } else {
                                Write-PSFMessage -Level Debug -Message "Move messageFileName dll file"
                                Move-Item -Path $messageFileNameFullName -Destination $DestinationPath -Force

                            }
                        }
                    }

                    if ($parameterFileNameFullName -notlike $resourceFileNameFullName) {
                        if ($pscmdlet.ShouldProcess("File parameter dll file '$($parameterFileNameFullName)' to '$($DestinationPath)'$(if($CopyMode){"in CopyMode"})", "Move")) {
                            if ($CopyMode) {
                                Write-PSFMessage -Level Debug -Message "Copy parameterFileName dll file"
                                Copy-Item -Path $parameterFileNameFullName -Destination $DestinationPath -Force
                            } else {
                                Write-PSFMessage -Level Debug -Message "Move parameterFileName dll file"
                                Move-Item -Path $parameterFileNameFullName -Destination $DestinationPath -Force
                            }
                        }
                    }

                    if ($PassThru) {
                        Write-PSFMessage -Level Verbose -Message "PassThru mode, outputting manifest file"
                        $destfile
                    }
                } else {
                    if ($PassThru) {
                        Write-PSFMessage -Level Verbose -Message "PassThru mode, outputting manifest file"
                        $file | Get-Item
                    }
                }
            }
        }
    }

    end {
    }
}

function New-WELCEventChannelManifest {
    <#
    .SYNOPSIS
        New-WELCEventChannelManifest
        Creates Manifest- and DLL-file for register custom Windows EventLog Channels
 
    .DESCRIPTION
        Creates Manifest- and DLL-file for register custom Windows EventLog Channels
 
        Once compiled, the files can be registered into a Windows EventLog system to
        allow custom logs.
 
    .PARAMETER InputObject
        WELC.Channeldefinition object to create manifest and the dll file from
 
        Can be piped in with somthing like:
        Import-WELCChannelDefinition -Path C:\EventLogs\WinEventLogCustomization.xlsx -OutputChannelDefinition
 
        Excel template file can be created/opened with Open-WELCExcelTemplate
 
    .PARAMETER ChannelFullName
        The full name for a EventChannel.
 
        Valid Formats are:
        "FolderRoot/ChannelName"
        "FolderRoot/SubFolder1/SubFolder2/ChannelName"
 
        String have to be specified correctly with slashes ("/"), NOT with backslash ("\")!
 
    .PARAMETER FolderRoot
        Name of the folder within the Windows EventLog System under "Application and Services"
 
    .PARAMETER FolderSecondLevel
        Name of the folder within the "FolderRoot"
 
    .PARAMETER FolderThirdLevel
        Name of the folder within the "FolderSecondLevel"
 
    .PARAMETER ChannelName
        Name of the EventChannel within it's folder
 
    .PARAMETER ChannelSymbol
        Name of the ChannelSymbol
 
        Optional. If not specified, the name will be derived from the ChannelName
 
    .PARAMETER ProviderName
        Name of the EventLog Provider
 
        Optional. If not specified, the name will be derived from the folder name(s)
 
 
    .PARAMETER ProviderSymbol
        Name of the ProviderSymbol
 
        Optional. If not specified, the name will be derived from the ProviderName
 
    .PARAMETER OutputFile
        The filename of the manifest to create.
        Please specify only a FILE name, not a full qualified path
 
        If not specified, the name will be derived from the ChannelFullName/ ChannelName
 
    .PARAMETER DestinationPath
        Output path for manifest- and the dll file to register an EventChannel within the Windows EventLog system
 
    .PARAMETER WhatIf
            If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
 
    .PARAMETER Confirm
            If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
 
    .NOTES
        Author: Andreas Bellstedt
 
        Adopted from Russell Tomkins "Project Sauron"
            Author: Russell Tomkins
            Github: https://www.github.com/russelltomkins/ProjectSauron
 
            Originbal description:
            ---------------------
            Name: Create-Manifest.ps1
            Version: 1.1
            Author: Russell Tomkins - Microsoft Premier Field Engineer
            Blog: https://aka.ms/russellt
            Refer to this blog series for more details
            http://blogs.technet.microsoft.com/russellt/2017/03/23/project-sauron-part-1
 
 
    .LINK
        https://github.com/AndiBellstedt/WinEventLogCustomization
 
    .EXAMPLE
        PS C:\> New-WELCEventChannelManifest -InputObject $ChannelDefinition
 
        Creates the manfifest- and DLL-file to register a custom EventLog channel in Windows
        Output depend on content in Excel file. Each root channel will be a manifest- (,man) and a DLL-file.
 
        Assuming that the variable $ChannelDefinition contains a WELC.ChannelDefinition object(list)
        PS C:\> $ChannelDefinition = Import-WELCChannelDefinition -Path CustomEventLogChannel.xlsx
 
    .EXAMPLE
        PS C:\> Import-WELCChannelDefinition -Path CustomEventLogChannel.xlsx | New-WELCEventChannelManifest
 
        Creates the Manfifest file and compile dll file(s) from the content of 'CustomEventLogChannel.xlsx'
 
    .EXAMPLE
        PS C:\> New-WELCEventChannelManifest -ChannelFullName "ChannelFolder/ChannelName"
 
        Creates a manifest (ChannelFolder.man) and compile a dll file (ChannelFolder.dll) with a single EventLogChannel "ChannelName" and a folder "ChannelFolder"
 
    .EXAMPLE
        PS C:\> "MyFolder/MyChannel1", "MyFolder/MyChannel2", "MyFolder/MyChannel3", "MyFolder/MyChannel4" | New-WELCEventChannelManifest
 
        Creates a manifest (MyChannel.man) and compile a dll file (MyChannel.dll) with 4 EventLogChannels MyChannel1-4 in the folder "MyFolder"
    #>

    [CmdletBinding(
        SupportsShouldProcess = $true,
        PositionalBinding = $true,
        ConfirmImpact = 'Medium',
        DefaultParameterSetName = 'ManualDefinition'
    )]
    Param(
        [Parameter(
            Mandatory = $true,
            ValueFromPipeline = $true,
            ParameterSetName = "InputObject",
            Position = 0
        )]
        [ValidateNotNullOrEmpty()]
        [Alias("Object", "In", "ChannelDefinition")]
        [WELC.ChannelDefinition[]]
        $InputObject,

        [Parameter(
            Mandatory = $true,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            ParameterSetName = "ManualFullChannelName",
            Position = 0
        )]
        [ValidateNotNullOrEmpty()]
        [Alias("FullName")]
        [String]
        $ChannelFullName,

        [Parameter(ParameterSetName = "ManualFullChannelName")]
        [String]
        $ChannelSymbol,

        [Parameter(ParameterSetName = "ManualFullChannelName")]
        [String]
        $ProviderName,

        [Parameter(ParameterSetName = "ManualFullChannelName")]
        [String]
        $ProviderSymbol,

        [Parameter(
            Mandatory = $true,
            ParameterSetName = "ManualDefinition",
            Position = 0
        )]
        [ValidateNotNullOrEmpty()]
        [Alias("Root", "FolderNameRoot", "RootFolderName")]
        [String]
        $FolderRoot,

        [Parameter(
            Mandatory = $false,
            ParameterSetName = "ManualDefinition",
            Position = 1
        )]
        [ValidateNotNullOrEmpty()]
        [Alias("SecondLevel", "FolderNameSecondLevel", "SecondLevelFolderName")]
        [String]
        $FolderSecondLevel,

        [Parameter(
            Mandatory = $false,
            ParameterSetName = "ManualDefinition",
            Position = 2
        )]
        [ValidateNotNullOrEmpty()]
        [Alias("ThirdLevel", "FolderNameThirdLevel", "ThirdLevelFolderName")]
        [String]
        $FolderThirdLevel,

        [Parameter(
            Mandatory = $true,
            ParameterSetName = "ManualDefinition",
            Position = 3
        )]
        [ValidateNotNullOrEmpty()]
        [String]
        $ChannelName,

        [Alias("FileName")]
        [String]
        $OutputFile,

        [ValidateNotNullOrEmpty()]
        [Alias("OutputPath")]
        [String]
        $DestinationPath = ".\"
    )

    Begin {
        #region Constants
        Write-PSFMessage -Level Debug -Message "Initalizing constants"
        # Path to csc.exe in windows
        [String]$WindowsCSCPath = "$($env:windir)\Microsoft.NET\Framework64\v4.0.30319"
        Write-PSFMessage -Level Debug -Message ".NET Framework in '$($WindowsCSCPath)'"

        # Compilation tools from the windows SDK. The required executables are "mc.exe", "rc.exe" and "rcdll.dll". There is another tool "ecmangen.exe" (EventChannel ManifestGenerator) which is usefull to check and maintain the manifest files.
        [String]$CompilationToolPath = "$($MyInvocation.MyCommand.Module.ModuleBase)\bin"
        Write-PSFMessage -Level Debug -Message "Binary CompilationTool is in '$($CompilationToolPath)'"

        # Path where the output files, and some other temp files from the compilation process are stored.
        [String]$TempPath = "$($env:TEMP)\WELC_$([guid]::NewGuid().guid)"
        Write-PSFMessage -Level Debug -Message "Operating in temporary path '$($TempPath)'"
        #endregion Constants


        #region Variables
        $channelDefinitions = @()
        #endregion Variables


        #region Validity checks
        Write-PSFMessage -Level Debug -Message "Initial parameter validation"
        # Check for required resscoures und compilation folder
        if ($DestinationPath.EndsWith('\')) { $DestinationPath = $DestinationPath.TrimEnd('\') }
        $DestinationPath = Resolve-Path $DestinationPath -ErrorAction Stop | Select-Object -ExpandProperty Path

        # Check for temp folder
        if ($TempPath.EndsWith('\')) { $TempPath = $TempPath.TrimEnd('\') }
        if (Test-Path -Path $TempPath -IsValid) {
            if (-not (Test-Path -Path $TempPath -PathType Container)) {
                Write-PSFMessage -Level Debug -Message "Creating temporary directory '$($TempPath)'"
                New-Item -Path $TempPath -ItemType Directory -Force | Out-Null
                $TempPath = Resolve-Path $TempPath -ErrorAction Stop | Select-Object -ExpandProperty Path
            }
        } else {
            throw "$($TempPath) is not a valid path"
        }

        # Check for required resscoures und compilation folder
        if ($CompilationToolPath.EndsWith('\')) { $CompilationToolPath = $CompilationToolPath.TrimEnd('\') }
        Resolve-Path $CompilationToolPath -ErrorAction Stop | Out-Null
        Test-Path -Path "$($CompilationToolPath)\mc.exe" -ErrorAction Stop | Out-Null
        Test-Path -Path "$($CompilationToolPath)\rc.exe" -ErrorAction Stop | Out-Null
        Test-Path -Path "$($CompilationToolPath)\rcdll.dll" -ErrorAction Stop | Out-Null
        Write-PSFMessage -Level Debug -Message "Binary tools found in CompilationTool path '$($CompilationToolPath)'"
        #endregion Validity checks
    }

    Process {
        Write-PSFMessage -Level Debug -Message "ParameterNameSet: $($PsCmdlet.ParameterSetName)"
        switch ($pscmdlet.ParameterSetName) {
            "InputObject" {
                $channelDefinitions += foreach ($item in $InputObject) {
                    $item
                }
            }

            "ManualFullChannelName" {
                $channelDefinitions += foreach ($_channelFullName in $ChannelFullName) {
                    # Validate the parameters - if SecondLevel is specified, ThirdLevel has to be present also
                    Write-PSFMessage -Level Debug -Message "Validating ChannelFullName '$($_channelFullName)'"
                    if ($_channelFullName -match (Get-PSFConfigValue -FullName WinEventLogCustomization.MatchString.ChannelName)) {
                        $_channelName = $_channelFullName
                    } else {
                        Stop-PSFFunction -Message "Invalid format on ChannelFullName '$($_channelFullName)'. Valid format for ChannelFullName must be somthing like 'FolderRoot-FolderSecondLevel-FolderThirdLevel/ChannelName' or 'FolderRoot/ChannelName'" -EnableException $true
                    }

                    if (-not $ChannelSymbol) {
                        $_channelSymbol = [String]::Join("_", $_channelFullName.Split("-").Split("/").ToUpper())
                        Write-PSFMessage -Level Debug -Message "ChannelSymbol not specified. Derive value '$($_channelSymbol)' from ChannelFullName"
                    } else {
                        if ($ChannelSymbol -match (Get-PSFConfigValue -FullName WinEventLogCustomization.MatchString.ChannelSymbol)) {
                            $_channelSymbol = $ChannelSymbol.ToUpper()
                        } else {
                            Stop-PSFFunction -Message "Invalid format on ChannelSymbol '$($ChannelSymbol)'. Valid format for ChannelSymbol must be somthing like 'FolderRoot_FolderSecondLevel_FolderThirdLevel_ChannelName' or 'FolderRoot_ChannelName'" -EnableException $true
                        }
                    }

                    if (-not $ProviderName) {
                        $_providerName = $_channelFullName.Replace( "/$($_channelFullName.Split("/")[-1])", "")
                        Write-PSFMessage -Level Debug -Message "ProviderName not specified. Derive value '$($_providerName)' from ChannelFullName"
                    } else {
                        if ($ProviderName -match (Get-PSFConfigValue -FullName WinEventLogCustomization.MatchString.ProviderName)) {
                            $_providerName = $ProviderName
                        } else {
                            Stop-PSFFunction -Message "Invalid format on ProviderName '$($ProviderName)'. Valid format for ProviderName must be somthing like 'FolderRoot-FolderSecondLevel-FolderThirdLevel' or 'FolderRoot'" -EnableException $true
                        }
                    }

                    if (-not $ProviderSymbol) {
                        $_providerSymbol = [String]::Join("_", ($_channelFullName.Replace( "/$($_channelFullName.Split("/")[-1])", "")).Split("-").ToUpper())
                        Write-PSFMessage -Level Debug -Message "ChannelSymbol not specified. Derive value '$($_providerSymbol)' from ChannelFullName"
                    } else {
                        if ($ProviderSymbol -match (Get-PSFConfigValue -FullName WinEventLogCustomization.MatchString.ProviderSymbol)) {
                            $_providerSymbol = $ProviderSymbol.ToUpper()
                        } else {
                            Stop-PSFFunction -Message "Invalid format on ProviderSymbol '$($ProviderSymbol)'. Valid Format for ProviderSymbol must be somthing like 'FolderRoot-FolderSecondLevel-FolderThirdLevel' or 'FolderRoot'" -EnableException $true
                        }

                    }

                    # Create custom "WEC.ChannelDefinition" object
                    Write-PSFMessage -Level Verbose -Message "Create ChannelDefinition object from '$($_channelFullName)'"
                    [PSCustomObject]@{
                        ProviderSymbol = $_providerSymbol
                        ProviderName   = $_providerName
                        ChannelName    = $_channelName
                        ChannelSymbol  = $_channelSymbol
                    }

                    # Cleanup the mess of variables
                    Write-PSFMessage -Level Debug -Message "Cleanup variables"
                    Remove-Variable _channelSymbol, _channelName, _providerSymbol, _providerName -Force -ErrorAction Ignore -WarningAction Ignore -Verbose:$false -Confirm:$false -WhatIf:$false -Debug:$false
                }
            }

            "ManualDefinition" {
                # Validate the parameters - if SecondLevel is specified, ThirdLevel has to be present also
                if ($FolderSecondLevel -and ($null -eq $FolderThirdLevel)) {
                    Write-PSFMessage -Level Warning -Message "Parameter 'FolderSecondLevel' was specified, but 'FolderThirdLevel' is missing."
                    Write-PSFMessage -Level Warning -Message "By design, only 'FolderRoot' or all the FolderPaths has to be specified."
                    Stop-PSFFunction -Message "Aborting creation."
                }

                # Build variables for custom ChannelDefinition object
                Write-PSFMessage -Level Debug -Message "Arranging data for ChannelDefinition object"
                [Array]$_folderNames = $FolderRoot, $FolderSecondLevel, $FolderThirdLevel | ForEach-Object { if ($_) { $_ } }
                [Array]$_providerSymbols = $FolderRoot.toupper(), $FolderSecondLevel.toupper(), $FolderThirdLevel.toupper() | ForEach-Object { if ($_) { $_ } }
                [Array]$_channelSymbols = $_providerSymbols + $ChannelName.toupper()

                $_providerName = [String]::Join("-", $_folderNames)
                $_providerSymbol = [String]::Join("_", $_providerSymbols)
                $_channelName = [String]::Join("-", $_folderNames) + "/" + $ChannelName
                $_channelSymbol = [String]::Join("_", $_channelSymbols)

                # Create custom "WEC.ChannelDefinition" object
                Write-PSFMessage -Level Verbose -Message "Create ChannelDefinition object for '$($_channelName)'"
                $channelDefinitions = [PSCustomObject]@{
                    ProviderSymbol = $_providerSymbol
                    ProviderName   = $_providerName
                    ChannelName    = $_channelName
                    ChannelSymbol  = $_channelSymbol
                }

                # Cleanup the mess of variables
                Write-PSFMessage -Level Debug -Message "Cleanup variables"
                Remove-Variable _channelSymbol, _channelName, _providerSymbol, _providerName, _channelSymbols, _providerSymbols, _folderNames -Force -ErrorAction Ignore -Confirm:$false -WhatIf:$false -Debug:$false
            }

            Default {
                throw "Undefined ParameterSet. Developers mistake."
            }
        }
    }

    End {
        Write-PSFMessage -Level Verbose -Message "Collected $($channelDefinitions.Count) channel definition$(if($channelDefinitions.Count -gt 1){"s"})"

        [array]$baseNames = $channelDefinitions | Select-Object -ExpandProperty ProviderName | Foreach-Object { $_.split("-")[0] } | Sort-Object -Unique
        Write-PSFMessage -Level Verbose -Message "Going to create $($baseNames.Count) manifest file$(if($baseNames.Count -gt 1){"s"}) from collected channel definition$(if($channelDefinitions.Count -gt 1){"s"})"
        foreach ($baseName in $baseNames) {
            # Shorten Name for file
            if ($pscmdlet.ParameterSetName -like "InputObject") {
                if ($OutputFile) {
                    $OutputFile = $OutputFile.Replace( ".$($OutputFile.Split(".")[-1])", "")
                    $fileName = $OutputFile + "_" + $baseName.Replace(" ", "")
                } else {
                    $fileName = $baseName.Replace(" ", "")
                }
            } else {
                if ($OutputFile) {
                    $fileName = $OutputFile.Replace( ".$($OutputFile.Split(".")[-1])", "")
                } else {
                    $fileName = $baseName.Replace(" ", "")
                }
            }

            # The Resource and Message DLL that will be referenced in the manifest
            $fileNameDLL = $fileName + ".dll"
            $fullNameDLLTemp = $TempPath + "\" + $fileNameDLL
            $fullNameDLLDestination = $DestinationPath + "\" + $fileNameDLL

            # The Manifest file
            $fileNameManifest = $fileName + ".man"
            $fullNameManifestTemp = $TempPath + "\" + $fileNameManifest

            Write-PSFMessage -Level Verbose -Message "Arraging manifest: $($fileName) ('$($DestinationPath + "\" + $fileNameManifest)', '$($fullNameDLLDestination)')"

            # Filter down the the full channel list
            $channelSelection = $channelDefinitions | Where-Object ProviderName -like "$($baseName)*"

            # Extract the provider information from input
            $providers = $channelSelection | Select-Object -Property ProviderSymbol, ProviderName -Unique | Foreach-Object { $_ | Select-Object *, @{n = "ProviderGuid"; e = { ([guid]::NewGuid()).Guid } } }

            #region Create the manifest XML document
            Write-PSFMessage -Level Verbose -Message "Working on group '$($baseName)' with $(([array]$channelSelection).Count) channel definitions in $(([array]$providers).count) folders"

            #region Basic XML object definition
            Write-PSFMessage -Level Debug -Message "Start building manifest XML document"
            # Create the manifest XML document
            $XmlWriter = [System.XMl.XmlTextWriter]::new($fullNameManifestTemp, $null)

            # Set the formatting
            $xmlWriter.Formatting = "Indented"
            $xmlWriter.Indentation = "4"

            # Write the XML decleration
            $xmlWriter.WriteStartDocument()

            # Create instrumentation manifest
            $xmlWriter.WriteStartElement("instrumentationManifest")
            $xmlWriter.WriteAttributeString("xsi:schemaLocation", "http://schemas.microsoft.com/win/2004/08/events eventman.xsd")
            $xmlWriter.WriteAttributeString("xmlns", "http://schemas.microsoft.com/win/2004/08/events")
            $xmlWriter.WriteAttributeString("xmlns:win", "http://manifests.microsoft.com/win/2004/08/windows/events")
            $xmlWriter.WriteAttributeString("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
            $xmlWriter.WriteAttributeString("xmlns:xs", "http://www.w3.org/2001/XMLSchema")
            $xmlWriter.WriteAttributeString("xmlns:trace", "http://schemas.microsoft.com/win/2004/08/events/trace")

            # Create instrumentation, events and provider elements
            $xmlWriter.WriteStartElement("instrumentation")
            $xmlWriter.WriteStartElement("events")
            #endregion Basic XML object definition

            foreach ($provider in $providers) {
                Write-PSFMessage -Level Verbose -Message "Writing provider '$($provider.ProviderName)' (GUID:$($provider.ProviderGUID))"
                # Start the provider
                $xmlWriter.WriteStartElement("provider")

                $xmlWriter.WriteAttributeString("name", $provider.ProviderName)
                $xmlWriter.WriteAttributeString("guid", "{$($provider.ProviderGUID)}")
                $xmlWriter.WriteAttributeString("symbol", $provider.ProviderSymbol)
                $xmlWriter.WriteAttributeString("resourceFileName", $fullNameDLLDestination)
                $xmlWriter.WriteAttributeString("messageFileName", $fullNameDLLDestination)
                $xmlWriter.WriteAttributeString("parameterFileName", $fullNameDLLDestination)

                # Start channels collection
                $xmlWriter.WriteStartElement("channels")

                [array]$channels = $channelSelection | Where-Object ProviderSymbol -eq $provider.ProviderSymbol | Select-Object -Property ChannelName, ChannelSymbol
                ForEach ($channelItem in $channels) {
                    Write-PSFMessage -Level Verbose -Message "Writing channel '$($channelItem.ChannelName)'"
                    # Start the channel
                    $xmlWriter.WriteStartElement("channel")

                    $xmlWriter.WriteAttributeString("name", $channelItem.ChannelName)
                    $xmlWriter.WriteAttributeString("chid", ($channelItem.ChannelName).Replace(' ', ''))
                    $xmlWriter.WriteAttributeString("symbol", $channelItem.ChannelSymbol)
                    $xmlWriter.WriteAttributeString("type", "Admin")
                    $xmlWriter.WriteAttributeString("enabled", "false")

                    # Closing the channel
                    $xmlWriter.WriteEndElement()
                }

                # Closing the channels
                $xmlWriter.WriteEndElement()

                # Closing the provider
                $xmlWriter.WriteEndElement()
            }

            #region Basic XML object definition
            $xmlWriter.WriteEndElement() # Closing events
            $xmlWriter.WriteEndElement() # Closing Instrumentation
            $xmlWriter.WriteEndElement() # Closing instrumentationManifest

            # End the XML Document
            $xmlWriter.WriteEndDocument()

            # Finish The Document
            $xmlWriter.Finalize
            $xmlWriter.Flush()
            $xmlWriter.Close()
            #endregion Basic XML object definition

            Write-PSFMessage -Level Verbose -Message "Manifest file '$($fileNameManifest)' has been generated ($( [math]::Round( ((Get-ChildItem -Path $fullNameManifestTemp).length / 1KB),1))KB)"
            #endregion Create The Manifest XML Document


            #region Compile the manifest to DLL
            Write-PSFMessage -Level Verbose -Message "Starting the compilation process on '$($fileNameDLL)'"
            $tempFilesExisting = @()
            $finalFilesExisting = @()
            $finalFilesExpected = @($fullNameManifestTemp, $fullNameDLLTemp)

            #region generates "**.h", "**.rc" and "**TEMP.BIN" file from xml manifest
            Write-PSFMessage -Level Debug -Message "Generate '$($fileName).h', '$($fileName).rc' and '$($fileName)TEMP.BIN' files from xml manifest"
            $tempFilesExpected = @("$($TempPath)\$($fileName).h", "$($TempPath)\$($fileName).rc", "$($TempPath)\$($fileName)TEMP.BIN")
            $tempFilesExpected | Get-ChildItem -ErrorAction SilentlyContinue | Remove-Item -Force -Confirm:$false
            Start-Process `
                -FilePath "$($CompilationToolPath)\mc.exe" `
                -ArgumentList $fullNameManifestTemp `
                -WorkingDirectory $TempPath `
                -NoNewWindow `
                -Wait

            Write-PSFMessage -Level Debug -Message "Validating generated files"
            foreach ($tempFile in $tempFilesExpected) {
                if (Test-Path -Path $tempFile -NewerThan (Get-Date).AddSeconds(-5)) {
                    $tempFilesExisting += Get-ChildItem $tempFile -ErrorAction Stop
                } else {
                    Stop-PSFFunction -Message "Expected temp file '$($tempFile)' is present, but has a too old timestamp. Something went wrong. Aborting process" -EnableException $true
                }
            }
            Write-PSFMessage -Level Debug -Message "File generated: $([string]::Join(", ", $tempFilesExpected))"
            #endregion generates "**.h", "**.rc" and "**TEMP.BIN" file from xml manifest

            #region generates "**.cs" file from xml manifest
            Write-PSFMessage -Level Debug -Message "Generate '$($fileName).cs' file from xml manifest"
            $tempFilesExpected = @( "$($TempPath)\$($fileName).cs" )
            $tempFilesExpected | Get-ChildItem -ErrorAction SilentlyContinue | Remove-Item -Force -Confirm:$false
            Start-Process `
                -FilePath "$($CompilationToolPath)\mc.exe" `
                -ArgumentList "-css NameSpace $($fullNameManifestTemp)" `
                -WorkingDirectory $TempPath `
                -NoNewWindow `
                -Wait

            Write-PSFMessage -Level Debug -Message "Validating generated '$($fileName).cs' file"
            foreach ($tempFile in $tempFilesExpected) {
                if (Test-Path -Path $tempFile -NewerThan (Get-Date).AddSeconds(-5)) {
                    $tempFilesExisting += Get-ChildItem $tempFile -ErrorAction Stop
                } else {
                    Stop-PSFFunction -Message "Expected temp file '$($tempFile)' is present, but has a too old timestamp. Something went wrong. Aborting process" -EnableException $true
                }
            }
            Write-PSFMessage -Level Debug -Message "CS file generated: $([string]::Join(", ", $tempFilesExpected)) "
            #endregion generates "**.cs" file from xml manifest

            #region generates "**.res" file from xml manifest
            Write-PSFMessage -Level Debug -Message "Generate '$fileName).res' file from '$($fileName).rc' file"
            $tempFilesExpected = @("$($TempPath)\$($fileName).res")
            $tempFilesExpected | Get-ChildItem -ErrorAction SilentlyContinue | Remove-Item -Force -Confirm:$false
            Start-Process `
                -FilePath "$($CompilationToolPath)\rc.exe" `
                -ArgumentList "$($fileName).rc" `
                -WorkingDirectory $TempPath `
                -Wait `
                -WindowStyle Hidden

            Write-PSFMessage -Level Debug -Message "Validating generated '$($fileName).res' file"
            foreach ($tempFile in $tempFilesExpected) {
                if (Test-Path -Path $tempFile -NewerThan (Get-Date).AddSeconds(-5)) {
                    $tempFilesExisting += Get-ChildItem $tempFile -ErrorAction Stop
                } else {
                    Stop-PSFFunction -Message "Expected temp file '$($tempFile)' is present, but has a too old timestamp. Something went wrong. Aborting process" -EnableException $true
                }
            }
            Write-PSFMessage -Level Debug -Message "Res file generated: $([string]::Join(", ", $tempFilesExpected)) "
            #endregion generates "**.res" file from xml manifest

            #region final compilation of the dll file
            Write-PSFMessage -Level Debug -Message "Finally compiling '$fileName).dll' file from generated meta files"
            Start-Process `
                -FilePath "$($WindowsCSCPath)\csc.exe" `
                -ArgumentList "/win32res:$($TempPath)\$($fileName).res /unsafe /target:library /out:$($TempPath)\$($fileName).dll $($TempPath)\$($fileName).cs" `
                -WorkingDirectory $TempPath `
                -Wait `
                -WindowStyle Hidden

            Write-PSFMessage -Level Debug -Message "Validating generated '$($fileName).dll' file"
            foreach ($FinalFile in $finalFilesExpected) {
                if (Test-Path -Path $FinalFile -NewerThan (Get-Date).AddSeconds(-15)) {
                    $finalFilesExisting += Get-ChildItem $FinalFile -ErrorAction Stop
                } else {
                    Stop-PSFFunction -Message "Expected temp file '$($FinalFile)' is present, but has a too old timestamp. Something went wrong. Aborting process" -EnableException $true
                }
            }
            Write-PSFMessage -Level Debug -Message "DLL file generated: $($TempPath)\$($fileName).dll"

            if ($pscmdlet.ShouldProcess("'$($fileNameManifest)' and '$($fileNameDLL)' in '$($DestinationPath)'", "Create")) {
                Write-PSFMessage -Level Verbose -Message "Writing final $($finalFilesExisting.Count) files to '$($DestinationPath)'"
                $finalFilesExisting | Copy-Item -Destination $DestinationPath -Force -ErrorAction Stop
            }
            #endregion final compilation of the dll file

            Write-PSFMessage -Level Verbose -Message "Finished process group '$($baseName)'"
            #endregion Compile the manifest to DLL
        }


        #region Cleanup
        Write-PSFMessage -Level Verbose -Message "Cleaning up temporary path '$($TempPath)'"
        Remove-Item -Path $TempPath -Force -Recurse -ErrorAction SilentlyContinue
        #endregion Cleanup
    }
}

function Open-WELCExcelTemplate {
    <#
        .Synopsis
            Open-WELCExcelTemplate
 
        .DESCRIPTION
            Open Excel template file for managing custom EventLog channel definition
 
            For obvious reason, Excel or equivalent tools needs to be present on the machine
 
        .EXAMPLE
            PS C:\> Open-WELCExcelTemplate
 
            Open a new Excel file from a template file within the module WindowsEventLogCustomization
 
        .NOTES
            Author: Andreas Bellstedt
 
        .LINK
            https://github.com/AndiBellstedt/WinEventLogCustomization
    #>

    [CmdLetBinding(
        SupportsShouldProcess = $false,
        ConfirmImpact = 'Low'
    )]
    param(
    )

    begin {
    }

    process {
    }

    end {
        $path = "$($ModuleRoot)\bin\WinEventLogCustomization.xltx"
        $pathExtension = $path.Split(".")[-1]
        $null = New-PSDrive -PSProvider registry -Root HKEY_CLASSES_ROOT -Name HKCR

        Write-PSFMessage -Level Debug -Message "Looking for application to open '$($pathExtension)' files"
        # parse registry for file extension
        $registryFileLinkInfo = Get-Item "HKCR:\.$($pathExtension)" -ErrorAction SilentlyContinue
        if ($registryFileLinkInfo) {
            # parse registry for linked appliaction info to open/ create new file from template
            $registryAppLinkInfo = Get-Item "HKCR:\$($registryFileLinkInfo.GetValue(''))\shell\new\command" -ErrorAction SilentlyContinue
            if (-not $registryAppLinkInfo) {
                Write-PSFMessage -Level Debug -Message "No 'create new' shell info found. Try to query 'shell open' info"
                $registryAppLinkInfo = Get-Item "HKCR:\$($registryFileLinkInfo.GetValue(''))\shell\Open\command" -ErrorAction SilentlyContinue
            }
        }

        if ($registryAppLinkInfo) {
            Write-PSFMessage -Level Debug -Message "Found application info '$($registryFileLinkInfo.GetValue(''))'. Parsing shell command '$($registryAppLinkInfo.GetValue(''))'"
            $invokeCmd = $registryAppLinkInfo.GetValue('') -replace "%1", $path
            $invokeCmd = ($invokeCmd -split '"') | Where-Object { $_ -and $_ -notlike " " } | ForEach-Object { $_.trim() }
        }

        if ($invokeCmd.Count -ge 2) {
            Write-PSFMessage -Level Debug -Message "Found application link '$($registryFileLinkInfo.GetValue(''))' in registry to open template file. Going to invoke '$($invokeCmd[0])' $($invokeCmd[-1])"

            if (Test-Path -Path $path) {
                Write-PSFMessage -Level Verbose -Message "Opening '$($path)'"
                Start-Process -FilePath $invokeCmd[0] -ArgumentList $invokeCmd[1 .. ($invokeCmd.count - 1)]
            } else {
                Write-PSFMessage -Level Error -Message "Missing template file in module. Unable to find '$($path)'"
            }
        } else {
            if ($registryFileLinkInfo) {
                Write-PSFMessage -Level Debug -Message "Unable parse shell command to open template file from registry, but file extension info is present in registry. Try to blindly invoke the template file item as a fallback"
                Invoke-Item -Path $path
            } else {
                Write-PSFMessage -Level Error -Message "Unable to open template file, due to no application for opening '$($pathExtension)-files' seems to be installed"
            }
        }
    }
}

function Register-WELCEventChannelManifest {
    <#
    .SYNOPSIS
        Register-WELCEventChannelManifest
 
    .DESCRIPTION
        Register a compiled DLL and the manifest file to windows EventLog sytem
        The content of the registered manifest appears in EventLog reader unter Application and Services Logs
 
    .PARAMETER Path
        The path to the manifest (and the dll) file
 
    .PARAMETER ComputerName
        The computer where to register the manifest file
 
    .PARAMETER Session
        PowerShell Session object where to register the manifest file
 
    .PARAMETER DestinationPath
        The path where to store the manifest and DLL file
 
        By default, this is the same as "Path", as long, as you do not specify something else.
        If you use remoting to register the manifest on a remote computer the files will be
        copied over locally into DestinationPath on the remote computer
 
    .PARAMETER Credential
        The credentials to use on remote calls
 
    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
 
    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
 
    .NOTES
        Author: Andreas Bellstedt
 
    .LINK
        https://github.com/AndiBellstedt/WinEventLogCustomization
 
    .EXAMPLE
        PS C:\> Register-WELCEventChannelManifest -Path C:\CustomDLLPath\MyChannel.man
 
        Register the manfifest-file to Windows EventLog System, so it appears in Application and Services Logs.
        Next to the MyChannel.man file, there has to be a MyChannel.dll.
 
        The manifest and DLL file will be registered from the path C:\CustomDLLPath and has to remain there.
 
    .EXAMPLE
        PS C:\> Register-WELCEventChannelManifest -Path C:\CustomDLLPath\MyChannel.man -DestinationPath $env:WinDir\System32
 
        Register the manfifest-file to Windows EventLog System, so it appears in Application and Services Logs.
        Next to the MyChannel.man file, there has to be a MyChannel.dll.
 
        The manifest and DLL file will be copied to the system32 directory of the current windows installation.
        From there it is registered and has to remain in that folder.
 
    .EXAMPLE
        PS C:\> Register-WELCEventChannelManifest -Path C:\CustomDLLPath\MyChannel.man -ComputerName SRV01
 
        Register the manfifest-file to Windows EventLog System on the remote computer "SRV01".
 
        The manifest and DLL file will be registered from the the local path "C:\CustomDLLPath" on "SRV01" and has to remain there.
 
    .EXAMPLE
        PS C:\> Register-WELCEventChannelManifest -Path C:\CustomDLLPath\MyChannel.man -Sesion $PSSession
 
        Register the manfifest-file to Windows EventLog System on all connections within the $PSSession variable
 
        Assuming $PSSession variable is created something like this:
        $PSSession = New-PSSession -ComputerName SRV01
    #>

    [CmdletBinding(
        SupportsShouldProcess = $true,
        PositionalBinding = $true,
        ConfirmImpact = 'Medium',
        DefaultParameterSetName = 'ComputerName'
    )]
    param (
        [Parameter(
            Mandatory = $true,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            Position = 0
        )]
        [ValidateNotNullOrEmpty()]
        [Alias("File", "FileName", "FullName")]
        [String[]]
        $Path,

        [Parameter(
            ParameterSetName = "ComputerName",
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            Position = 1
        )]
        [Alias("Host", "Hostname", "Computer", "DNSHostName")]
        [PSFComputer[]]
        $ComputerName = $env:COMPUTERNAME,

        [Parameter(
            ParameterSetName = "Session",
            Position = 1
        )]
        [System.Management.Automation.Runspaces.PSSession[]]
        $Session,

        [Parameter(ParameterSetName = "ComputerName")]
        [PSCredential]
        $Credential,

        [String]
        $DestinationPath
    )

    begin {
        # If session parameter is used -> transfer it to ComputerName,
        # The class "PSFComputer" from PSFramework can handle it. This simplifies the handling in the further process block
        if ($Session) { $ComputerName = $Session.ComputerName }
        $DestinationPath = $DestinationPath.TrimEnd("\")

        $pathBound = Test-PSFParameterBinding -ParameterName Path
        $computerBound = Test-PSFParameterBinding -ParameterName ComputerName
    }

    process {
        #region parameterset workarround
        Write-PSFMessage -Level Debug -Message "ParameterNameSet: $($PsCmdlet.ParameterSetName)"

        # Workarround parameter binding behaviour of powershell in combination with ComputerName Piping
        if (-not ($pathBound -or $computerBound) -and $ComputerName.InputObject -and $PSCmdlet.ParameterSetName -ne "Session") {
            if ($ComputerName.InputObject -is [string]) { $ComputerName = $env:ComputerName } else { $Path = "" }
        }
        #endregion parameterset workarround

        #region Processing Events
        foreach ($pathItem in $Path) {
            # File and folder validation
            if (Test-Path -Path $pathItem -PathType Leaf) {
                Write-PSFMessage -Level Verbose -Message "Found file '$($pathItem)' as a valid file in path" -Target $env:COMPUTERNAME
                $files = $pathItem | Resolve-Path | Get-ChildItem | Select-Object -ExpandProperty FullName
            } elseif (Test-Path -Path $pathItem -PathType Container) {
                Write-PSFMessage -Level Verbose -Message "Getting files in path '$($pathItem)'" -Target $env:COMPUTERNAME
                $files = Get-ChildItem -Path $pathItem -File -Filter "*.man" | Select-Object -ExpandProperty FullName
                Write-PSFMessage -Level Verbose -Message "Found $($files.count) file$(if($files.count -gt 1){"s"}) in path" -Target $env:COMPUTERNAME
                if (-not $files) { Write-PSFMessage -Level Warning -Message  "No manifest files found in path '$($pathItem)'" -Target $env:COMPUTERNAME }
            } elseif (-not (Test-Path  -Path $pathItem -PathType Any -IsValid)) {
                Write-PSFMessage -Level Error -Message "'$pathItem' is not a valid path or file." -Target $env:COMPUTERNAME
                continue
            } else {
                Write-PSFMessage -Level Error -Message "unable to open '$($pathItem)'" -Target $env:COMPUTERNAME
                continue
            }

            foreach ($file in $files) {
                if (-not $DestinationPath) { $DestinationPath = Split-Path -Path $file }

                # Check for dll paths in manifest / prepare dll paths in manifest for different destination path
                if (
                    (-not (Test-WELCEventChannelManifest -Path $file -OnlyDLLPath)) -or
                    ((split-path $file) -notlike $DestinationPath)
                ) {
                    [String]$tempPath = "$($env:TEMP)\WELC_$([guid]::NewGuid().guid)"
                    if (Test-Path -Path $tempPath -IsValid) {
                        if (-not (Test-Path -Path $tempPath -PathType Container)) {
                            New-Item -Path $tempPath -ItemType Directory -Force | Out-Null
                            $tempPath = Resolve-Path $tempPath -ErrorAction Stop | Select-Object -ExpandProperty Path
                        }
                    }
                    Write-PSFMessage -Level Verbose -Message "Prepare '$($file)' for destination '$($DestinationPath)'"

                    $tempFile = Move-WELCEventChannelManifest -Path $file -DestinationPath $tempPath -CopyMode -PassThru -ErrorAction Stop | Select-Object -ExpandProperty FullName

                    $file = Move-WELCEventChannelManifest -Path $tempFile -DestinationPath $DestinationPath -Prepare -PassThru | Select-Object -ExpandProperty FullName
                }

                Write-PSFMessage -Level Verbose -Message "Opening XML manifest file '$($file)' to gather DLL information"
                $xmlfile = New-Object XML
                $xmlfile.Load($file)

                $dllFiles = @()

                Write-PSFMessage -Level Debug -Message "Gather path of resourceFileName DLL"
                $dllFileList = $xmlfile.instrumentationManifest.instrumentation.events.provider.resourceFileName
                foreach ($dllFile in $dllFileList) {
                    if ((Test-Path -Path $dllFile -PathType Leaf) -and ((Split-Path -Path $dllFile) -notlike $DestinationPath)) {
                        $dllFiles += $dllFile
                    } else {
                        $dllFile = "$(split-path $file)\$(Split-Path -Path $dllFile -Leaf)"
                        if (Test-Path -Path $dllFile -PathType Leaf) {
                            $dllFiles += $dllFile
                        } else {
                            Stop-PSFFunction -Message "Unexpected behavior while locating ressource dll file"
                        }
                    }
                }

                Write-PSFMessage -Level Debug -Message "Gather path of messageFileName DLL"
                $dllFileList = $xmlfile.instrumentationManifest.instrumentation.events.provider.messageFileName
                foreach ($dllFile in $dllFileList) {
                    if ((Test-Path -Path $dllFile -PathType Leaf) -and ((Split-Path -Path $dllFile) -notlike $DestinationPath)) {
                        $dllFiles += $dllFile
                    } else {
                        $dllFile = "$(split-path $file)\$(Split-Path -Path $dllFile -Leaf)"
                        if (Test-Path -Path $dllFile -PathType Leaf) {
                            $dllFiles += $dllFile
                        } else {
                            Stop-PSFFunction -Message "Unexpected behavior while locating message dll file"
                        }
                    }
                }

                Write-PSFMessage -Level Debug -Message "Gather path of parameterFileName DLL"
                $dllFileList = $xmlfile.instrumentationManifest.instrumentation.events.provider.parameterFileName
                foreach ($dllFile in $dllFileList) {
                    if ((Test-Path -Path $dllFile -PathType Leaf) -and ((Split-Path -Path $dllFile) -notlike $DestinationPath)) {
                        $dllFiles += $dllFile
                    } else {
                        $dllFile = "$(split-path $file)\$(Split-Path -Path $dllFile -Leaf)"
                        if (Test-Path -Path $dllFile -PathType Leaf) {
                            $dllFiles += $dllFile
                        } else {
                            Stop-PSFFunction -Message "Unexpected behavior while locating parameter dll file"
                        }
                    }
                }

                $dllFiles = $dllFiles | Sort-Object -Unique
                Write-PSFMessage -Level Verbose -Message "Found $($dllFiles.count) dll: $([string]::Join(", ", $dllFiles))"


                # Process computers
                foreach ($computer in $ComputerName) {
                    Write-PSFMessage -Level Verbose -Message "Processing file '$($file)' on computer '$($computer)'"

                    # When remoting is used, transfer files first
                    if (($PSCmdlet.ParameterSetName -eq "Session") -or (-not $computer.IsLocalhost)) {
                        Write-PSFMessage -Level Verbose -Message "Going to transfer file into destination '$($DestinationPath)' on remote computer"

                        if ($pscmdlet.ShouldProcess("Manifest '$($file)' and dll to computer '$($computer)'", "Transfer")) {

                            # Create PS remoting session
                            if ($PSCmdlet.ParameterSetName -ne "Session") {
                                $paramSession = @{
                                    "ComputerName" = $computer.ToString()
                                    "ErrorAction"  = "Stop"
                                }
                                if ($Credential) { $paramSession.Add("Credential", $Credential) }

                                try {
                                    $Session = New-PSSession @paramSession
                                } catch {
                                    Write-PSFMessage -Level Error -Message "Error creating remoting session to computer '$($computer)'" -Target $computer -ErrorRecord $_
                                    break
                                }
                            }

                            # Transfer files
                            Copy-Item -ToSession $Session -Destination $DestinationPath -Force -Path $file
                            Copy-Item -ToSession $Session -Destination $DestinationPath -Force -Path $dllFiles
                        }
                    } elseif ((split-path $file) -notlike $DestinationPath) {
                        Write-PSFMessage -Level Verbose -Message "Going to copy file into destination '$($DestinationPath)'"
                        Copy-Item -Destination $DestinationPath -Force -Path $file
                        Copy-Item -Destination $DestinationPath -Force -Path $dllFiles
                    }

                    # Register manifest
                    if ($pscmdlet.ShouldProcess("Manifest '$($file)' on computer '$($computer)'", "Register")) {

                        $destFileName = "$($DestinationPath)\$(split-path $file -Leaf)"
                        $paramInvokeCmd = [ordered]@{
                            "ComputerName" = $computer.ToString()
                            "ErrorAction"  = "Stop"
                            ErrorVariable  = "ErrorReturn"
                            "ArgumentList" = $destFileName
                        }
                        if ($PSCmdlet.ParameterSetName -eq "Session") { $paramInvokeCmd['ComputerName'] = $Session }
                        if ($Credential) { $paramInvokeCmd.Add("Credential", $Credential) }

                        Write-PSFMessage -Level Verbose -Message "Registering manifest '$($destFileName)' on computer '$($computer)'" -Target $computer
                        try {
                            $null = Invoke-PSFCommand @paramInvokeCmd -ScriptBlock {
                                try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch { Write-Information -MessageData "Exception while setting UTF8 OutputEncoding. Continue script." }
                                $output = . "$($env:windir)\system32\wevtutil.exe" "install-manifest" "$($args[0])" *>&1
                                $output = $output | Where-Object { $_.InvocationInfo.MyCommand.Name -like 'wevtutil.exe' } *>&1
                                if ($output) { Write-Error -Message "$([string]::Join(" ", $output.Exception.Message.Replace("`r`n"," ")))" -ErrorAction Stop }
                            }
                            if ($ErrorReturn) { Write-Error "Error registering manifest" -ErrorAction Stop }
                        } catch {
                            Stop-PSFFunction -Message "Unable to register manifest '$($destFileName)' on computer '$($computer)'" -Target $computer -ErrorRecord $_
                        }

                    }
                }

                if ($tempPath) {
                    Write-PSFMessage -Level Debug -Message "Cleaning up temporary directory '$($tempPath)'"
                    Remove-Item -Path $tempPath -Recurse -Force -Confirm:$false -WhatIf:$false -Verbose:$false -Debug:$false -ErrorAction:Ignore
                }
            }
        }
    }

    end {
    }
}

function Set-WELCEventChannel {
    <#
    .SYNOPSIS
        Set-WELCEventChannel
 
    .DESCRIPTION
        Set various properties on a EventChannel
 
    .PARAMETER ChannelConfig
        InputObject (WELC.ChannelConfig) from Excel Template file
 
        Can be created by
            Import-WELCChannelDefinition -Path ".\MyTemplate.xls" -OutputChannelConfig
 
    .PARAMETER EventChannel
        InputObject from Get-WELCEventChannel
 
        Available Alias on the parameter: EventLogChannel
 
    .PARAMETER ChannelFullName
        The name of the channel to be set
 
    .PARAMETER ComputerName
        The computer where to set the configuration
 
    .PARAMETER Enabled
        The status of the logfile. By default the logfiles are enabled after execution
 
    .PARAMETER LogMode
        Specifies how events in the EventChannel are treated, if the EventChannel reaches the maximum.
 
        Possilibilites:
            "AutoBackup" = File from EventChannel will be renamed and a newly file will be created
            "Circular" = Oldest event will be overwritten
            "Retain" = Newly events will be refused
 
    .PARAMETER LogFilePath
        The path in the filesystem for the EventChannel EVTX file
 
        This can be a full qualified filename - if only ONE EventChannel is to be set/ piped in.
        Effectivly this means a rename of the file for the EventChannel.
 
        If multiple configurations should be set/ piped in, the value on this parameter
        should be a FOLDER, not a file!
 
    .PARAMETER CompressLogFolder
        Specifies if the folder with the log files get compressed
 
    .PARAMETER MaxEventLogSize
        The maximum size in bytes for the EventChannel
 
    .PARAMETER AllowFileAccessForLocalService
        The WellKnownPrincipal 'Local Service' will add in the NTFS ACL to gain access to the logfile
 
        If the channel is planned to be used in "Windows Event Forwarding" this should probably set to true
 
    .PARAMETER EventChannelSDDL
        SDDL string to set access on the eventlog within the WinodwsEventLog system itself
 
        This access object controls who can view events in the EventLog for this channel
        So it's not on the filesystem, it's in MMC, WMI or PowerShell
 
    .PARAMETER PassThru
        The moved files will be parsed to the pipeline for further processing.
 
    .PARAMETER WhatIf
            If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
 
    .PARAMETER Confirm
            If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
 
    .NOTES
        Author: Andreas Bellstedt
 
        This a is quite far modified version from Project Sauron fork.
            Name: Prep-EventChannels.ps1
            Version: 1.1
            Author: Russell Tomkins - Microsoft Premier Field Engineer
            Blog: https://aka.ms/russellt
 
            Preparation of event channels to receive event collection subscriptions from an input CSV
            Source: https://www.github.com/russelltomkins/ProjectSauron
 
            Refer to this blog series for more details
            http://blogs.technet.microsoft.com/russellt/2017/03/23/project-sauron-part-1
 
    .LINK
        https://github.com/AndiBellstedt/WinEventLogCustomization
 
 
    .EXAMPLE
        PS C:\> Set-WELCEventChannel -ChannelFullName "App1/MyLog" -LogMode Circular -LogFilePath "C:\EventLogs\App1-MyLog.evtx"
 
        Set the 'MyLog' EventLog in the EventFolder 'App1' to circular logging and the path of the logfile to 'C:\EventLogs\App1-MyLog.evtx'
 
    .EXAMPLE
        PS C:\> $channels | Set-WELCEventChannel -Enabled $true -MaxEventLogSize 1GB
 
        Enables the EventChannels from the $channels variable and set maximum size to 1GB.
 
        Assuming the $channels variable is filled with something like
        $channels = Get-WELCEventChannel -ChannelFullName "App1/MyLog", "App2/MyLog"
 
    .EXAMPLE
        PS C:\> $ChannelConfig | Set-WELCEventChannel -Enabled $true -CompressLogFolder $true -AllowFileAccessForLocalService $true
 
        Enables the EventChannels from the $ChannelConfig variable and set all the properties within the $ChannelConfig variable.
        Additionally, the logfile/logfolder for the EventLogs will be compressed (except if it is a folder in Windows\System32),
        and the SID for "Local Service" gain Read/Write-Access.
 
        Assuming the $channels variable is filled with something like
        $ChannelConfig = Import-WELCChannelDefinition -Path C:\EventLogs\WinEventLogCustomization.xlsx -OutputChannelConfig
 
        Excel template file can be created/opened with Open-WELCExcelTemplate
 
    #>

    [CmdletBinding(
        DefaultParameterSetName = "ChannelName",
        SupportsShouldProcess = $true,
        PositionalBinding = $true,
        ConfirmImpact = 'Medium'
    )]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", '', Justification = "Intentional, Pester not covering the usage correct")]
    Param(
        [Parameter(
            ParameterSetName = "TemplateChannelConfig",
            Mandatory = $true,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            Position = 0
        )]
        [ValidateNotNullOrEmpty()]
        [WELC.ChannelConfig[]]
        $ChannelConfig,

        [Parameter(
            ParameterSetName = "EventChannel",
            Mandatory = $true,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            Position = 0
        )]
        [Alias("EventLogChannel")]
        [WELC.EventLogChannel[]]
        $EventChannel,

        [Parameter(
            ParameterSetName = "ChannelName",
            Mandatory = $true,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            Position = 0
        )]
        [Alias("ChannelName")]
        [String[]]
        $ChannelFullName,

        [Parameter(
            ParameterSetName = "TemplateChannelConfig",
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            Position = 1
        )]
        [Parameter(
            ParameterSetName = "ChannelName",
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            Position = 1
        )]
        [Alias("Host", "Hostname", "Computer", "DNSHostName")]
        [PSFComputer[]]
        $ComputerName = $env:COMPUTERNAME,

        [Parameter(ParameterSetName = "EventChannel")]
        [Parameter(ParameterSetName = "ChannelName")]
        [bool]
        $Enabled,

        [Parameter(ParameterSetName = "EventChannel")]
        [Parameter(ParameterSetName = "ChannelName")]
        [int]
        $MaxEventLogSize,

        [Parameter(ParameterSetName = "EventChannel")]
        [Parameter(ParameterSetName = "ChannelName")]
        [ValidateSet("AutoBackup", "Circular", "Retain")]
        [string]
        $LogMode,

        [Parameter(ParameterSetName = "EventChannel")]
        [Parameter(ParameterSetName = "ChannelName")]
        [String]
        $LogFilePath,

        [Parameter(ParameterSetName = "EventChannel")]
        [Parameter(ParameterSetName = "ChannelName")]
        [Alias("Compress")]
        [bool]
        $CompressLogFolder,

        [bool]
        $AllowFileAccessForLocalService,

        [String]
        $EventChannelSDDL,

        [switch]
        $PassThru
    )

    Begin {
        $channelFullNameBound = Test-PSFParameterBinding -ParameterName ChannelFullName
        $computerBound = Test-PSFParameterBinding -ParameterName ComputerName^

        $configList = New-Object System.Collections.ArrayList

        # validation on parameter LogFilePath
        if ($LogFilePath) {
            $LogFilePath = $LogFilePath.TrimEnd("\")

            if ($LogFilePath -like '%SystemRoot%*') { $LogFilePath = $LogFilePath.Replace('%SystemRoot%', $env:SystemRoot) }

            if ($LogFilePath.EndsWith(".evtx")) {
                $logFileFolder = Split-Path -Path $LogFilePath
                $logFileFullName = $LogFilePath
            } else {
                $logFileFolder = $LogFilePath
                $logFileFullName = "ToBeCalculated"
            }
        }

        # if compression is set and windows-folder is specified as LogFilePath -> abort, to avoid 'unhealthy' system modification
        if ($LogFilePath -like "$($env:SystemRoot)\System32\*" -and $CompressLogFolder -eq $true) {
            Stop-PSFFunction -Message "Hardcoded exception, not going to set compression within windows-System32-folder. Aborting function" -EnableException $true
            break
        }
    }

    Process {
        #region parameterset workarround
        Write-PSFMessage -Level Debug -Message "ParameterNameSet: $($PsCmdlet.ParameterSetName)"

        #ToDo: Check if this behaves right. What is if ComputerName "foo" is piped in and Channelfullname is specified, and vice versa
        # Workarround parameter binding behaviour of powershell in combination with ComputerName Piping
        if (-not ($channelFullNameBound -or $computerBound) -and $ComputerName.InputObject) {
            if ($ComputerName.InputObject -is [string]) { $ComputerName = $env:ComputerName } else { $ChannelFullName = "" }
        }
        #endregion parameterset workarround


        switch ($pscmdlet.ParameterSetName) {
            "TemplateChannelConfig" {
                Write-PSFMessage -Level Verbose -Message "Gathering $(([array]$ChannelConfig).count) channel configurations"
                foreach ($channelConfigItem in $ChannelConfig) {
                    foreach ($computer in $ComputerName) {
                        $eventChannel = $null
                        $eventChannel = Get-WELCEventChannel -ChannelFullName $channelConfigItem.ChannelName -ComputerName $computer -ErrorAction SilentlyContinue
                        if ($eventChannel) {
                            foreach ($eventChannelItem in $eventChannel) {
                                Write-PSFMessage -Level Debug -Message "Collecting config object for '$($eventChannelItem.Name)' on '$($computer)'"
                                $null = $configList.Add(
                                    [PSCustomObject]@{
                                        EventChannel                   = $eventChannelItem
                                        Enabled                        = $channelConfigItem.Enabled
                                        MaxEventLogSize                = $channelConfigItem.MaxEventLogSize
                                        LogMode                        = $channelConfigItem.LogMode
                                        LogFileFullName                = $channelConfigItem.LogFullName
                                        LogFilePath                    = (Split-Path -Path $channelConfigItem.LogFullName)
                                        CompressLogFolder              = $CompressLogFolder
                                        AllowFileAccessForLocalService = $AllowFileAccessForLocalService
                                        EventChannelSDDL               = $EventChannelSDDL
                                    }
                                )
                            }
                        } else {
                            Write-PSFMessage -Level Warning -Message "Skipping '$($channelConfigItem.ChannelName)' on '$($computer)'"
                            continue
                        }
                    }
                }
            }

            "EventChannel" {
                foreach ($eventChannelItem in $EventChannel) {
                    Write-PSFMessage -Level Debug -Message "Collecting config object for '$($eventChannelItem.ChannelFullName)' on '$($eventChannelItem.ComputerName)'"
                    $null = $configList.Add(
                        [PSCustomObject]@{
                            EventChannel                   = $eventChannelItem
                            Enabled                        = ( if (Test-PSFParameterBinding -ParameterName Enabled) { $Enabled } )
                            MaxEventLogSize                = ( if (Test-PSFParameterBinding -ParameterName MaxEventLogSize) { $MaxEventLogSize } )
                            LogMode                        = ( if (Test-PSFParameterBinding -ParameterName LogMode) { $LogMode } )
                            LogFileFullName                = ( if ($logFileFullName -like "ToBeCalculated") { "$($logFileFolder)\$($eventChannelItem.LogFile)" } elseif (Test-PSFParameterBinding -ParameterName LogFilePath) { $logFileFullName } )
                            LogFilePath                    = ( if (Test-PSFParameterBinding -ParameterName LogFilePath) { $logFileFolder } )
                            CompressLogFolder              = ( if (Test-PSFParameterBinding -ParameterName CompressLogFolder) { $CompressLogFolder } )
                            AllowFileAccessForLocalService = ( if (Test-PSFParameterBinding -ParameterName AllowFileAccessForLocalService) { $AllowFileAccessForLocalService } )
                            EventChannelSDDL               = ( if (Test-PSFParameterBinding -ParameterName EventChannelSDDL) { $EventChannelSDDL } )
                        }
                    )
                }
            }

            "ChannelName" {
                foreach ($channelNameItem in $ChannelFullName) {
                    foreach ($computer in $ComputerName) {
                        $eventChannel = $null
                        $eventChannel = Get-WELCEventChannel -ChannelFullName $channelNameItem -ComputerName $computer -ErrorAction SilentlyContinue
                        if ($eventChannel) {
                            foreach ($eventChannelItem in $eventChannel) {
                                Write-PSFMessage -Level Debug -Message "Collecting config object for '$($eventChannelItem.Name)' on '$($computer)'"
                                $null = $configList.Add(
                                    [PSCustomObject]@{
                                        EventChannel                   = $eventChannelItem
                                        Enabled                        = ( if (Test-PSFParameterBinding -ParameterName Enabled) { $Enabled } )
                                        MaxEventLogSize                = ( if (Test-PSFParameterBinding -ParameterName MaxEventLogSize) { $MaxEventLogSize } )
                                        LogMode                        = ( if (Test-PSFParameterBinding -ParameterName LogMode) { $LogMode } )
                                        LogFileFullName                = ( if ($logFileFullName -like "ToBeCalculated") { "$($logFileFolder)\$($eventChannelItem.LogFile)" } elseif (Test-PSFParameterBinding -ParameterName LogFilePath) { $logFileFullName } )
                                        LogFilePath                    = ( if (Test-PSFParameterBinding -ParameterName LogFilePath) { $logFileFolder } )
                                        CompressLogFolder              = ( if (Test-PSFParameterBinding -ParameterName CompressLogFolder) { $CompressLogFolder } )
                                        AllowFileAccessForLocalService = ( if (Test-PSFParameterBinding -ParameterName AllowFileAccessForLocalService) { $AllowFileAccessForLocalService } )
                                        EventChannelSDDL               = ( if (Test-PSFParameterBinding -ParameterName EventChannelSDDL) { $EventChannelSDDL } )
                                    }
                                )
                            }
                        } else {
                            Write-PSFMessage -Level Warning -Message "Skipping '$($channelNameItem)' on '$($computer)'"
                            continue
                        }
                    }
                }
            }

            Default {
                Stop-PSFFunction -Message "Unhandeled ParameterSetName. Developers mistake." -EnableException $true
                throw
            }
        }

    }

    End {
        # invalid configuration attempt - Parameter 'LogFilePath' specified as file and multiple channels should be configured
        if ($pscmdlet.ParameterSetName -notlike "TemplateChannelConfig" -and $LogFilePath -and $logFileFullName -notlike "ToBeCalculated" -and $configList.count -gt 1) {
            Stop-PSFFunction -Message "Parameter 'LogFilePath' was specified as a file, but more than one EventChannels where specified/piped. This leads to unvalid configuration. Each EventChannel has to have it's own LogFile. Please specify a folder on parameter 'LogFilePath'. Aborting..." -EnableException $true
            throw
        }

        Write-PSFMessage -Level Verbose -Message "Working trough list of $($configList.Count) collected EventChannel$(if($configList.Count -gt 1){"s"}) to configure"
        foreach ($configItem in $configList) {
            Write-PSFMessage -Level Verbose -Message "Processing '$($configItem.EventChannel.Name)' on '$($configItem.EventChannel.PSComputerName)'"

            # Get current folder for EventLogFile
            $eventLogFolderCurrent = Invoke-PSFCommand -ComputerName $configItem.EventChannel.PSComputerName -ArgumentList $configItem.EventChannel.LogFolder -ScriptBlock {
                $_query = "select * from CIM_Directory where name = `"$($args[0].Replace('\','\\'))`""
                Get-CimInstance -Query $_query -ErrorAction SilentlyContinue -Verbose:$false -Debug:$false
            }

            # Check if FilePath for EventChannel should be modified
            if ($configItem.LogFilePath -and ($eventLogFolderCurrent.Name -notlike $configItem.LogFilePath)) {
                Write-PSFMessage -Level Verbose -Message "Set new path '$($configItem.LogFilePath)' for EventChannel"

                # First, check if destination folder is already present
                Write-PSFMessage -Level Debug -Message "Test new path: $($configItem.LogFilePath)"
                $destinationFolder = Invoke-PSFCommand -ComputerName $configItem.EventChannel.PSComputerName -ArgumentList $configItem.LogFilePath -ErrorVariable invokeErrors -ScriptBlock {
                    $_query = "select * from CIM_Directory where name = `"$($args[0].Replace('\','\\'))`""
                    $_result = Get-CimInstance -Query $_query -ErrorAction Ignore -Verbose:$false -Debug:$false

                    # if folder is not present, try to query root folder, to check if drive is valid
                    if (-not $_result) { $null = Get-Item -Path "$($args[0].split("\")[0])\" -ErrorAction SilentlyContinue } else { $_result }
                }

                # if error occured = drive is not present/valid
                if ($invokeErrors.Exception) {
                    Write-PSFMessage -Level Error -Message "Invalid path '$($configItem.LogFilePath)' on system '$($configItem.EventChannel.PSComputerName)'! Possibly inaccessable drive/volume" -EnableException $true -Exception $invokeErrors.Exception -ErrorRecord $invokeErrors -Target $configItem.EventChannel.PSComputerName
                    continue
                }
                Remove-Variable -Name invokeErrors -Force -Confirm:$false -WhatIf:$false -Verbose:$false -Debug:$false -ErrorAction Ignore

                # if folder is not available, create it
                If (-not $destinationFolder) {
                    if ($pscmdlet.ShouldProcess("EventChannel '$($configItem.EventChannel.Name)' on computer '$($configItem.EventChannel.PSComputerName)'", "Create folder '$($configItem.LogFilePath)'")) {
                        Write-PSFMessage -Level Verbose -Message "Create folder '$($configItem.LogFilePath)' for EventChannel"

                        try {
                            $destinationFolder = Invoke-PSFCommand -ComputerName $configItem.EventChannel.PSComputerName -ArgumentList $configItem.LogFilePath -ErrorAction Stop -ErrorVariable invokeErrors -ScriptBlock {
                                $_folder = New-Item -Type Directory -Path $args[0] -Force -ErrorAction Stop
                                $_query = "select * from CIM_Directory where name = `"$($_folder.FullName.Replace('\','\\'))`""
                                Get-CimInstance -Query $_query -ErrorAction SilentlyContinue -Verbose:$false -Debug:$false
                            }
                        } catch {
                            Write-PSFMessage -Level Error -Message "Unable to create folder '$($configItem.LogFilePath)' on '$($configItem.EventChannel.PSComputerName)'" -EnableException $true -Exception $_.Exception -ErrorRecord $_ -Target $configItem.EventChannel.PSComputerName
                            continue
                        }
                    }
                }

                # Set new folder to EventChannel
                if ($pscmdlet.ShouldProcess("EventChannel '$($configItem.EventChannel.Name)' on computer '$($configItem.EventChannel.PSComputerName)'", "Set new destination '$($configItem.LogFileFullName)'")) {
                    Write-PSFMessage -Level Verbose -Message "Set new destination '$($configItem.LogFileFullName)' for EventChannel '$($configItem.EventChannel.Name)' on computer '$($configItem.EventChannel.PSComputerName)'"

                    $invokeParam = @{
                        "ComputerName"  = $configItem.EventChannel.PSComputerName
                        "ArgumentList"  = ($configItem.EventChannel.Name , $configItem.LogFileFullName)
                        "ErrorAction"   = "Stop"
                        "ErrorVariable" = "invokeErrors"
                    }
                    try {
                        Invoke-PSFCommand @invokeParam -ScriptBlock {
                            $_eventChannelName = $args[0]
                            $_logFileFullName = $args[1]

                            $_channel = Get-WinEvent -ListLog $_eventChannelName -Force -ErrorAction Stop
                            $error.Clear()

                            # backup current status
                            $_currentIsEnabled = $_channel.IsEnabled

                            # Disable to change settings
                            $_channel.IsEnabled = $false
                            $_channel.SaveChanges()

                            # Set path on channel
                            $_channel.LogFilePath = $_logFileFullName
                            $_channel.IsEnabled = $_currentIsEnabled
                            $_channel.SaveChanges()
                            if ($error.Exception) { Write-Error "" -ErrorAction Stop }
                        }
                    } catch {
                        Write-PSFMessage -Level Error -Message "Unable to set new destination '$($configItem.LogFileFullName)' on '$($configItem.EventChannel.PSComputerName)'" -EnableException $true -Exception $_.Exception -ErrorRecord $_ -Target $configItem.EventChannel.PSComputerName
                    }
                }
            } else {
                $destinationFolder = $eventLogFolderCurrent
            }
            Remove-Variable -Name invokeErrors -Force -Confirm:$false -WhatIf:$false -Verbose:$false -Debug:$false -ErrorAction Ignore


            # Add an ACE to allow LOCAL SERVICE to modify the folder
            if ($configItem.AllowFileAccessForLocalService) {
                if ($pscmdlet.ShouldProcess("EventChannel '$($configItem.EventChannel.Name)' on computer '$($configItem.EventChannel.PSComputerName)'", "Add ACE for 'localSystem' on folder '$($configItem.LogFilePath)'")) {
                    Write-PSFMessage -Level Verbose -Message "Set ntfs permission for 'local service' to folder '$($destinationFolder.name)' on '$($configItem.EventChannel.PSComputerName)'"

                    $invokeParam = @{
                        "ComputerName"  = $configItem.EventChannel.PSComputerName
                        "ArgumentList"  = ($destinationFolder.name)
                        "ErrorAction"   = "Stop"
                        "ErrorVariable" = "invokeErrors"
                    }
                    try {
                        Invoke-PSFCommand @invokeParam -ScriptBlock {
                            $_destinationFolderName = $args[0]
                            $ace = New-Object System.Security.AccessControl.FileSystemAccessRule(
                                [System.Security.Principal.SecurityIdentifier]::new("S-1-5-19").Translate([System.Security.Principal.NTAccount]).Value,
                                'Modify',
                                'ContainerInherit,ObjectInherit',
                                'None',
                                'Allow'
                            )

                            $logPathACL = $_destinationFolderName | Get-Item -ErrorAction Stop | Get-ACL -ErrorAction Stop
                            if (-not ($logPathACL.Access | Where-Object { $_.IdentityReference -like $ace.IdentityReference -and $_.FileSystemRights -like $ace.FileSystemRights -and $_.AccessControlType -like $ace.AccessControlType })) {
                                $logPathACL.AddAccessRule($ace)
                                $logPathACL | Set-ACL -ErrorAction Stop
                            }
                        }
                    } catch {
                        Write-PSFMessage -Level Error -Message "Unable to set ntfs permission for 'local service' to folder '$($destinationFolder.name)' on '$($configItem.EventChannel.PSComputerName)'. Message: $($_.Exception.Message)" -EnableException $true -Exception $_.Exception -ErrorRecord $_ -Target $configItem.EventChannel.PSComputerName
                        continue
                    }
                    Remove-Variable -Name invokeErrors -Force -Confirm:$false -WhatIf:$false -Verbose:$false -Debug:$false -ErrorAction Ignore
                }
            }

            # Set compression on folder
            if ($configItem.CompressLogFolder) {
                if ($pscmdlet.ShouldProcess("EventChannel '$($configItem.EventChannel.Name)' on computer '$($configItem.EventChannel.PSComputerName)'", "Set compression '$($configItem.CompressLogFolder)' on folder '$($configItem.LogFilePath)'")) {
                    Write-PSFMessage -Level Verbose -Message "Set compression '$($configItem.CompressLogFolder)' to folder '$($destinationFolder)' on '$($configItem.EventChannel.PSComputerName)'"

                    $invokeParam = @{
                        "ComputerName"    = $configItem.EventChannel.PSComputerName
                        "ArgumentList"    = ($destinationFolder, $configItem.CompressLogFolder)
                        "ErrorAction"     = "Stop"
                        "ErrorVariable"   = "invokeErrors"
                        "WarningAction"   = "SilentlyContinue"
                        "WarningVariable" = "invokeWarnings"
                    }
                    try {
                        Invoke-PSFCommand @invokeParam -ScriptBlock {
                            $_destinationFolder = $args[0]
                            $_Compression = $args[1]
                            $_query = "select * from CIM_Directory where name = `"$($_destinationFolder.Name.Replace('\','\\'))`""
                            $_cimResult = $null
                            $returnCodes = [ordered]@{
                                0  = "Success"
                                2  = "Access denied"
                                8  = "Unspecified failure"
                                9  = "Invalid object"
                                10 = "Object already exists"
                                11 = "File system not NTFS"
                                12 = "Platform not Windows"
                                13 = "Drive not the same"
                                14 = "Directory not empty"
                                15 = "Sharing violation"
                                16 = "Invalid start file"
                                17 = "Privilege not held"
                                21 = "Invalid parameter"
                            }

                            if ($_Compression -eq $true -and $_destinationFolder.Compressed -eq $false) {
                                $_cimResult = Invoke-CimMethod -Query $_query -MethodName Compress -ErrorAction Stop -Verbose:$false
                            } elseif ($_Compression -eq $false -and $_destinationFolder.Compressed -eq $true) {
                                $_cimResult = Invoke-CimMethod -Query $_query -MethodName Uncompress -ErrorAction Stop -Verbose:$false
                            } else {
                                Write-Warning "Noting to do on '$($_destinationFolder.Name)'" -WarningAction SilentlyContinue
                            }

                            if ($_cimResult) {
                                if ($_cimResult.ReturnValue -notin (0, 15)) {
                                    Write-Error "Error '$($_cimResult.ReturnValue) ($($returnCodes[$_cimResult.ReturnValue]))' occured while set compression attribute '$($_Compression)'" -ErrorAction Stop
                                } elseif ($_cimResult.ReturnValue -eq 15) {
                                    Write-Warning "Compression attribute set, but with sharing violation. Means, attribute could not be applied on all files in folder" -WarningAction SilentlyContinue
                                }
                            }
                        }
                    } catch {
                        Write-PSFMessage -Level Error -Message "Unable to set compression to folder '$($destinationFolder.name)' on '$($configItem.EventChannel.PSComputerName)'. Message: $($_.Exception.Message)" -EnableException $true -Exception $_.Exception -ErrorRecord $_ -Target $configItem.EventChannel.PSComputerName
                    }
                    if ($invokeWarnings) { $invokeWarnings | ForEach-Object { Write-PSFMessage -Level Warning -Message $_ } }
                    Remove-Variable -Name invokeErrors, invokeWarnings -Force -Confirm:$false -WhatIf:$false -Verbose:$false -Debug:$false -ErrorAction Ignore
                }
            }

            # Set all the other possible EventChannel configs, including "Enabled" as the last setting
            $valueToSet = $configItem.psobject.Properties | Where-Object Name -in ("Enabled", "MaxEventLogSize", "LogMode", "EventChannelSDDL") | Where-Object { $_.Value -is $_.TypeNameOfValue }
            if ($valueToSet) {
                $valueToSetText = [string]::Join(', ', ($valueToSet | ForEach-Object { "$($_.Name)=$($_.Value)" }))
                if ($pscmdlet.ShouldProcess("EventChannel '$($configItem.EventChannel.Name)' on computer '$($configItem.EventChannel.PSComputerName)'", "Set '$($valueToSetText)'")) {
                    Write-PSFMessage -Level Verbose -Message "Set '$($valueToSetText)' to EventChannel '$($configItem.EventChannel.Name)' on '$($configItem.EventChannel.PSComputerName)'"

                    $invokeParam = @{
                        "ComputerName"  = $configItem.EventChannel.PSComputerName
                        "ArgumentList"  = ($configItem.EventChannel.Name, ($valueToSet | Select-Object Name, Value))
                        "ErrorAction"   = "Stop"
                        "ErrorVariable" = "invokeErrors"
                    }
                    try {
                        Invoke-PSFCommand @invokeParam -ScriptBlock {
                            $_eventChannelName = $args[0]
                            $_valueToSet = $args[1]
                            $_translateSettingToProperty = @{
                                "Enabled"          = "IsEnabled"
                                "MaxEventLogSize"  = "MaximumSizeInBytes"
                                "LogMode"          = "LogMode"
                                "EventChannelSDDL" = "SecurityDescriptor"
                            }

                            $_channel = Get-WinEvent -ListLog $_eventChannelName -Force -ErrorAction Stop
                            $error.Clear()

                            # backup current status
                            $_currentIsEnabled = $_channel.IsEnabled

                            # Disable to change settings
                            $_channel.IsEnabled = $false
                            $_channel.SaveChanges()

                            # Set properties on channel
                            foreach ($setting in ($_valueToSet | Where-Object name -NotLike "Enabled")) {
                                $_channel.($_translateSettingToProperty[$setting.Name]) = $setting.Value
                            }

                            $_desiredChannelStatus = $_valueToSet | Where-Object name -Like "Enabled"
                            if ($_desiredChannelStatus) {
                                $_channel.IsEnabled = $_desiredChannelStatus.Value
                            } else {
                                $_channel.IsEnabled = $_currentIsEnabled
                            }
                            $_channel.SaveChanges()
                            if ($error.Exception) { Write-Error "" -ErrorAction Stop }
                        }
                    } catch {
                        Write-PSFMessage -Level Error -Message "Unable to set '$($valueToSetText)' to EventChannel '$($configItem.EventChannel.Name)' on '$($configItem.EventChannel.PSComputerName)'. Message: $($_.Exception.Message)" -EnableException $true -Exception $_.Exception -ErrorRecord $_ -Target $configItem.EventChannel.PSComputerName
                        continue
                    }
                    Remove-Variable -Name invokeErrors -Force -Confirm:$false -WhatIf:$false -Verbose:$false -Debug:$false -ErrorAction Ignore
                }
            }

            # Output if specified
            if ($PassThru) {
                Get-WELCEventChannel -ChannelFullName $configItem.EventChannel.Name -ComputerName $configItem.EventChannel.PSComputerName -ErrorAction SilentlyContinue
            }
        }
    }
}


function Test-WELCEventChannelManifest {
    <#
    .SYNOPSIS
        Test-WELCEventChannelManifest
 
    .DESCRIPTION
        Test a man file for valid path with the compiled DLL file belonging to manifest file
 
        The manifest has to contain the fullname of the path where the dll file is stored,
        otherwise there will be errors when registering/usering it
 
    .PARAMETER Path
        The path to the manifest file
 
    .PARAMETER OnlyDLLPath
        Only verify path of DLL files in Manifest and skip validation of properties
 
    .PARAMETER Property
        Explicitly validate only the specified property
 
    .PARAMETER PassThru
        The moved files will be parsed to the pipeline for further processing.
 
    .NOTES
        Author: Andreas Bellstedt
 
    .LINK
        https://github.com/AndiBellstedt/WinEventLogCustomization
 
    .EXAMPLE
        PS C:\> Test-WELCEventChannelManifest -Path C:\CustomDLLPath\MyChannel.man
 
        Test the manifest. Show $true, if the manifest is a valid EventLogChannelManifest and the compiled DLL file ist in the expected directory
        Otherwise $false will be the result of the test.
 
    .EXAMPLE
        PS C:\> Test-WELCEventChannelManifest -Path C:\CustomDLLPath\MyChannel.man -OnlyDLLPath
 
        Same like first example, but skip structure and name checks. In fact, it only checks on reference path for DLL file.
 
    .EXAMPLE
        PS C:\> Test-WELCEventChannelManifest -Path C:\CustomDLLPath\MyChannel.man -PassThru
 
        Test the manifest. If the manifest is a valid EventLogChannelManifest and the compiled DLL file ist in the expected directory,
        the path (fullname) of the Manifest file will be the output
 
    #>

    [CmdletBinding(
        SupportsShouldProcess = $false,
        PositionalBinding = $true,
        ConfirmImpact = 'Low',
        DefaultParameterSetName = "General"
    )]
    [OutputType("System.Boolean")]
    param (
        [Parameter(
            Mandatory = $true,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            Position = 0
        )]
        [ValidateNotNullOrEmpty()]
        [Alias("File", "FileName", "FullName")]
        [String[]]
        $Path,

        [Parameter(ParameterSetName = "General")]
        [switch]
        $OnlyDLLPath,

        [Parameter(
            Mandatory = $true,
            ParameterSetName = "ExplicitProperty"
        )]
        [ValidateSet("ProviderName", "ProviderGUID", "ProviderSymbol", "ResourceFileName", "MessageFileName", "ParameterFileName", "ChannelName", "ChannelSymbol", "Type", "Enabled")]
        [String[]]
        $Property,

        [switch]
        $PassThru
    )

    begin {

    }

    process {
        foreach ($pathItem in $Path) {
            $isOK = $true
            # File and folder validity tests
            if ((Test-Path -Path $pathItem -PathType Leaf) -and ($pathItem.Split(".")[-1] -like "man")) {
                $file = $pathItem | Resolve-Path | Get-ChildItem | Select-Object -ExpandProperty FullName
                Write-PSFMessage -Level Verbose -Message "Found file '$($file )' as a valid file"
            } elseif (Test-Path -Path $pathItem -PathType Container) {
                Write-PSFMessage -Level Error -Message "'$pathItem' is a folder. Please specify a manifest file."
                continue
            } elseif (-not (Test-Path  -Path $pathItem -PathType Any -IsValid)) {
                Write-PSFMessage -Level Error -Message "'$pathItem' is not a valid path or file."
                continue
            } else {
                Write-PSFMessage -Level Error -Message "Unable to open '$($pathItem)'"
                continue
            }

            # open XML file
            $xmlfile = New-Object XML
            $xmlfile.Load($file)

            if (
                $xmlfile.instrumentationManifest.schemaLocation -eq "http://schemas.microsoft.com/win/2004/08/events eventman.xsd" -and
                $xmlfile.instrumentationManifest.xmlns -eq "http://schemas.microsoft.com/win/2004/08/events" -and
                $xmlfile.instrumentationManifest.win -eq "http://manifests.microsoft.com/win/2004/08/windows/events" -and
                $xmlfile.instrumentationManifest.xsi -eq "http://www.w3.org/2001/XMLSchema-instance" -and
                $xmlfile.instrumentationManifest.xs -eq "http://www.w3.org/2001/XMLSchema" -and
                $xmlfile.instrumentationManifest.trace -eq "http://schemas.microsoft.com/win/2004/08/events/trace"
            ) {
                # Loop through existing providers
                foreach ($provider in $xmlfile.instrumentationManifest.instrumentation.events.provider) {
                    # Check Provider info
                    if (-not $OnlyDLLPath -or $pscmdlet.ParameterSetName -like "ExplicitProperty") {
                        if ($pscmdlet.ParameterSetName -like "General" -or "ProviderGUID" -in $Property) {
                            if ([guid]::new($provider.guid)) {
                                Write-PSFMessage -Level Debug -Message "GUID '$($provider.guid)' for provider '$($provider.name)' is valid"
                            } else {
                                Write-PSFMessage -Level Verbose -Message "Failed testing GUID '$($provider.guid)' for provider '$($provider.name)'"
                                $isOK = $false
                            }
                        }

                        if ($pscmdlet.ParameterSetName -like "General" -or "ProviderName" -in $Property) {
                            if ($provider.name -match (Get-PSFConfigValue -FullName WinEventLogCustomization.MatchString.ProviderName)) {
                                Write-PSFMessage -Level Debug -Message "Name for provider '$($provider.name)' is valid"
                            } else {
                                Write-PSFMessage -Level Verbose -Message "Failed testing provider name '$($provider.name)'. Name did not match '$(Get-PSFConfigValue -FullName WinEventLogCustomization.MatchString.ProviderName)'"
                                $isOK = $false
                            }
                        }

                        if ($pscmdlet.ParameterSetName -like "General" -or "ProviderSymbol" -in $Property) {
                            if ($provider.symbol -match (Get-PSFConfigValue -FullName WinEventLogCustomization.MatchString.ProviderSymbol)) {
                                Write-PSFMessage -Level Debug -Message "Providersymbol '$($provider.symbol)' is valid"
                            } else {
                                Write-PSFMessage -Level Verbose -Message "Failed testing provider symbol '$($provider.symbol)'. Name did not match '$(Get-PSFConfigValue -FullName WinEventLogCustomization.MatchString.ProviderSymbol)'"
                                $isOK = $false
                            }
                        }
                    }

                    if ($pscmdlet.ParameterSetName -like "General" -or "ResourceFileName" -in $Property) {
                        if (Test-Path -Path $provider.resourceFileName -PathType Leaf) {
                            Write-PSFMessage -Level Debug -Message "Ressource file '$($provider.resourceFileName)' for provider '$($provider.name)' GUID:$($provider.guid) is valid"
                        } else {
                            Write-PSFMessage -Level Verbose -Message "Failed testing ressource file '$($provider.resourceFileName)' for provider '$($provider.name)' GUID:$($provider.guid)"
                            $isOK = $false
                        }
                    }

                    if ($pscmdlet.ParameterSetName -like "General" -or "MessageFileName" -in $Property) {
                        if (Test-Path -Path $provider.messageFileName -PathType Leaf) {
                            Write-PSFMessage -Level Debug -Message "Message file '$($provider.messageFileName)' for provider '$($provider.name)' GUID:$($provider.guid) is valid"
                        } else {
                            Write-PSFMessage -Level Verbose -Message "Failed testing message file '$($provider.messageFileName)' for provider '$($provider.name)' GUID:$($provider.guid)"
                            $isOK = $false
                        }
                    }

                    if ($pscmdlet.ParameterSetName -like "General" -or "ParameterFileName" -in $Property) {
                        if (Test-Path -Path $provider.parameterFileName -PathType Leaf) {
                            Write-PSFMessage -Level Debug -Message "Parameter file '$($provider.parameterFileName)' for provider '$($provider.name)' GUID:$($provider.guid) is valid"
                        } else {
                            Write-PSFMessage -Level Verbose -Message "Failed testing parameter file '$($provider.parameterFileName)' for provider '$($provider.name)' GUID:$($provider.guid)"
                            $isOK = $false
                        }
                    }

                    if (-not $OnlyDLLPath -or $pscmdlet.ParameterSetName -like "ExplicitProperty") {
                        # Loop through channels within provider
                        foreach ($channel in $provider.channels.channel) {
                            # Check Channel info
                            if ($pscmdlet.ParameterSetName -like "General" -or "ChannelName" -in $Property) {
                                if ($channel.name -match (Get-PSFConfigValue -FullName WinEventLogCustomization.MatchString.ChannelName)) {
                                    Write-PSFMessage -Level Debug -Message "Name for ChannelName '$($channel.name)' in provider '$($provider.name)' is valid"
                                } else {
                                    Write-PSFMessage -Level Verbose -Message "Failed testing ChannelName '$($channel.name)' in provider '$($provider.name)'. Name did not match '$(Get-PSFConfigValue -FullName WinEventLogCustomization.MatchString.ChannelName)'"
                                    $isOK = $false
                                }
                            }

                            if ($pscmdlet.ParameterSetName -like "General" -or "ChannelSymbol" -in $Property) {
                                if ($channel.symbol -match (Get-PSFConfigValue -FullName WinEventLogCustomization.MatchString.ChannelSymbol)) {
                                    Write-PSFMessage -Level Debug -Message "Name for ChannelSymbol '$($channel.symbol)' in provider '$($provider.name)' is valid"
                                } else {
                                    Write-PSFMessage -Level Verbose -Message "Failed testing ChannelSymbol '$($channel.symbol)' in provider '$($provider.name)'. Name did not match '$(Get-PSFConfigValue -FullName WinEventLogCustomization.MatchString.ChannelSymbol)'"
                                    $isOK = $false
                                }
                            }

                            if ($pscmdlet.ParameterSetName -like "General" -or "Enabled" -in $Property) {
                                if (($channel.enabled -like [bool]::TrueString) -or ($channel.enabled -like [bool]::FalseString)) {
                                    Write-PSFMessage -Level Debug -Message "Value Enabled:'$($channel.enabled)' on channel '$($channel.name)' in provider '$($provider.name)' is valid"
                                } else {
                                    Write-PSFMessage -Level Verbose -Message "Failed testing value '$($channel.enabled)' on channel '$($channel.name)' in provider '$($provider.name)'."
                                    $isOK = $false
                                }
                            }

                            if ($pscmdlet.ParameterSetName -like "General" -or "Type" -in $Property) {
                                if ($channel.type -in (Get-PSFConfigValue -FullName WinEventLogCustomization.MatchString.ChannelTypes)) {
                                    Write-PSFMessage -Level Debug -Message "Type '$($channel.type)' on channel '$($channel.name)' in provider '$($provider.name)' is valid"
                                } else {
                                    Write-PSFMessage -Level Verbose -Message "Failed testing type '$($channel.type)' on channel '$($channel.name)' in provider '$($provider.name)'."
                                    $isOK = $false
                                }
                            }
                        }
                    }
                }
            } else {
                Write-PSFMessage -Level Error -Message "'$($file)' seeams like not being a Windows EventLog Channel XML manifest file"
                $isOK = $false
            }

            # Output result
            if ($PassThru -and $isOK) {
                $file
            } else {
                $isOK
            }
        }
    }

    end {
    }
}

function Unregister-WELCEventChannelManifest {
    <#
    .SYNOPSIS
        Unregister-WELCEventChannelManifest
 
    .DESCRIPTION
        Unregister a manifest and its compiled DLL file from windows EventLog sytem
 
    .PARAMETER Path
        The path to the manifest (and the dll) file
 
    .PARAMETER ComputerName
        The computer where to register the manifest file
 
    .PARAMETER Session
        PowerShell Session object where to unregister the manifest file
 
    .PARAMETER Credential
        The credentials to use on remote calls
 
    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
 
    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
 
    .NOTES
        Author: Andreas Bellstedt
 
    .LINK
        https://github.com/AndiBellstedt/WinEventLogCustomization
 
    .EXAMPLE
        PS C:\> Unregister-WELCEventChannelManifest -Path C:\CustomDLLPath\MyChannel.man
 
        Unregister the manfifest-file from Windows EventLog System, so it no longer appears in Application and Services Logs.
 
    .EXAMPLE
        PS C:\> Unregister-WELCEventChannelManifest -Path C:\CustomDLLPath\MyChannel.man -ComputerName SRV01
 
        Unregister the manfifest-file from Windows EventLog System on the remote computer "SRV01".
 
    .EXAMPLE
        PS C:\> Unregister-WELCEventChannelManifest -Path C:\CustomDLLPath\MyChannel.man -Sesion $PSSession
 
        Unregister the manfifest-file from Windows EventLog System from all connections within the $PSSession variable
 
        Assuming $PSSession variable is created something like this:
        $PSSession = New-PSSession -ComputerName SRV01
 
    #>

    [CmdletBinding(
        SupportsShouldProcess = $true,
        PositionalBinding = $true,
        ConfirmImpact = 'High',
        DefaultParameterSetName = 'ComputerName'
    )]
    param (
        [Parameter(
            Mandatory = $true,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            Position = 0
        )]
        [ValidateNotNullOrEmpty()]
        [Alias("File", "FileName", "FullName")]
        [String]
        $Path,

        [Parameter(
            ParameterSetName = "ComputerName",
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            Position = 1
        )]
        [Alias("Host", "Hostname", "Computer", "DNSHostName")]
        [PSFComputer[]]
        $ComputerName = $env:COMPUTERNAME,

        [Parameter(
            ParameterSetName = "Session",
            Position = 1
        )]
        [System.Management.Automation.Runspaces.PSSession[]]
        $Session,

        [Parameter(ParameterSetName = "ComputerName")]
        [PSCredential]
        $Credential
    )

    begin {
        # If session parameter is used -> transfer it to ComputerName,
        # The class "PSFComputer" from PSFramework can handle it. This simplifies the handling in the further process block
        if ($Session) { $ComputerName = $Session.ComputerName }

        $pathBound = Test-PSFParameterBinding -ParameterName Path
        $computerBound = Test-PSFParameterBinding -ParameterName ComputerName
    }

    process {
        #region parameterset workarround
        Write-PSFMessage -Level Debug -Message "ParameterNameSet: $($PsCmdlet.ParameterSetName)"

        # Workarround parameter binding behaviour of powershell in combination with ComputerName Piping
        if (-not ($pathBound -or $computerBound) -and $ComputerName.InputObject -and $PSCmdlet.ParameterSetName -ne "Session") {
            if ($ComputerName.InputObject -is [string]) { $ComputerName = $env:ComputerName } else { $Path = "" }
        }
        #endregion parameterset workarround

        #region Processing Events
        foreach ($file in $Path) {
            # File/path validation
            if (-not (Test-Path -Path $file -PathType Leaf -IsValid)) {
                Write-PSFMessage -Level Error -Message"'$($file)' is not a valid path or file."
                continue
            } else {
                Write-PSFMessage -Level Debug -Message "Working on file '$($file)'"
            }

            # Process computers
            foreach ($computer in $ComputerName) {
                Write-PSFMessage -Level Verbose -Message "Processing file '$($file)' on computer '$($computer)'"

                # When remoting is used, transfer files first
                if (($PSCmdlet.ParameterSetName -eq "Session") -or (-not $computer.IsLocalhost)) {

                    # Create PS remoting session, if no session exists
                    if ($PSCmdlet.ParameterSetName -ne "Session") {

                        $paramSession = @{
                            "ComputerName" = $computer.ToString()
                            "ErrorAction"  = "Stop"
                        }
                        if ($Credential) { $paramSession.Add("Credential", $Credential) }

                        try {
                            $Session = New-PSSession @paramSession
                            Write-PSFMessage -Level Debug -Message "New remoting session created to '$($Session.ComputerName)'"
                        } catch {
                            Write-PSFMessage -Level Error -Message "Error creating remoting session to computer '$($computer)'" -Target $computer -ErrorRecord $_
                            break
                        }
                    }
                }

                # Register manifest
                if ($pscmdlet.ShouldProcess("Manifest '$($Path)' from computer '$($computer)'", "Unregister")) {

                    $paramInvokeCmd = [ordered]@{
                        "ComputerName" = $computer.ToString()
                        "ErrorAction"  = "Stop"
                        ErrorVariable  = "ErrorReturn"
                        "ArgumentList" = $file
                    }
                    if ($PSCmdlet.ParameterSetName -eq "Session") { $paramInvokeCmd['ComputerName'] = $Session }
                    if ($Credential) { $paramInvokeCmd.Add("Credential", $Credential) }

                    Write-PSFMessage -Level Verbose -Message "Unregistering manifest '$($file)' from computer '$($computer)'" -Target $computer
                    try {
                        $null = Invoke-PSFCommand @paramInvokeCmd -ScriptBlock {
                            try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch { Write-Information -MessageData "Exception while setting UTF8 OutputEncoding. Continue script." }
                            $output = . "$($env:windir)\system32\wevtutil.exe" "uninstall-manifest" "$($args[0])" *>&1
                            $output = $output | Where-Object { $_.InvocationInfo.MyCommand.Name -like 'wevtutil.exe' } *>&1
                            if ($output) { Write-Error -Message "$([string]::Join(" ", $output.Exception.Message.Replace("`r`n"," ")))" -ErrorAction Stop }
                        }
                        if ($ErrorReturn) { Write-Error "Error registering manifest" -ErrorAction Stop }
                    } catch {
                        Stop-PSFFunction -Message "Error unregistering manifest '$($file)' on computer '$($computer)'" -Target $computer -ErrorRecord $_
                    }
                }
            }
        }
    }

    end {
    }
}

<#
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 'WinEventLogCustomization' -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 'WinEventLogCustomization' -Name 'Import.DoDotSource' -Value $false -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." -Initialize
Set-PSFConfig -Module 'WinEventLogCustomization' -Name 'Import.IndividualFiles' -Value $false -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." -Initialize


Set-PSFConfig -Module 'WinEventLogCustomization' -Name 'MatchString.ProviderName' -Value '(^([a-zA-Z]| |[0-9]|\(|\))*$)|(^([a-zA-Z]| |[0-9]|\(|\))*-([a-zA-Z]| |[0-9]|\(|\))*-([a-zA-Z]| |[0-9]|\(|\))*$)' -Description "Regex to validate name of ProviderName within a manifest file" -Initialize
Set-PSFConfig -Module 'WinEventLogCustomization' -Name 'MatchString.ProviderSymbol' -Value '(^([a-zA-Z]| |[0-9]|\(|\))*$)|(^([a-zA-Z]| |[0-9]|\(|\))*_([a-zA-Z]| |[0-9]|\(|\))*_([a-zA-Z]| |[0-9]|\(|\))*$)|(^([a-zA-Z]| |[0-9]|\(|\))*_([a-zA-Z]| |[0-9]|\(|\))*_([a-zA-Z]| |[0-9]|\(|\))*_([a-zA-Z]| |[0-9]|\(|\))*$)'-Description "Regex to validate name of ProviderSymbol within a manifest file" -Initialize

Set-PSFConfig -Module 'WinEventLogCustomization' -Name 'MatchString.ChannelName' -Value '(^(\w| )*\/(\w| |\(|\))*$)|(^(\w| )*-(\w| )*-(\w| )*\/(\w| |\(|\))*$)'-Description "Regex to validate name of ChannelName within a manifest file" -Initialize
Set-PSFConfig -Module 'WinEventLogCustomization' -Name 'MatchString.ChannelSymbol' -Value '(^([a-zA-Z]|[0-9]|\(|\))*_([a-zA-Z]|[0-9]|\(|\))*$)|(^([a-zA-Z]|[0-9]|\(|\))*_([a-zA-Z]|[0-9]|\(|\))*_([a-zA-Z]|[0-9]|\(|\))*_([a-zA-Z]|[0-9]|\(|\))*$)' -Description "Regex to validate name of ChannelSymbol within a manifest file" -Initialize
Set-PSFConfig -Module 'WinEventLogCustomization' -Name 'MatchString.ChannelTypes' -Value @("Admin", "Operational", "Analytic", "Debug") -Validation stringarray -Description "Name-array of possible ChannelTypes" -Initialize


<#
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 'WinEventLogCustomization.ScriptBlockName' -Scriptblock {
     
}
#>


Register-PSFTeppScriptblock -Name "WinEventLogCustomization.ChannelFullName" -ScriptBlock {
    Get-WinEvent -ListLog * -ErrorAction Ignore | Select-Object -ExpandProperty LogName
}

Register-PSFTeppScriptblock -Name "WinEventLogCustomization.Bool" -ScriptBlock {
    @(
        '$true',
        '$false'
    )
}

Register-PSFTeppScriptblock -Name "WinEventLogCustomization.MaxEventLogSize" -ScriptBlock {
    @(
        '16MB',
        '64MB',
        '128MB',
        '512MB',
        '1GB',
        '2GB',
        '5GB',
        '10GB'
    )
}

Register-PSFTeppScriptblock -Name "WinEventLogCustomization.FolderRoot" -ScriptBlock {
    Get-WinEvent -ListLog *-* -ErrorAction Ignore | Select-Object -ExpandProperty LogName | ForEach-Object { $_.split("-")[0] } | Sort-Object -Unique
}

Register-PSFTeppScriptblock -Name "WinEventLogCustomization.FolderSecondLevel" -ScriptBlock {
    Get-WinEvent -ListLog *-*-* -ErrorAction Ignore | Select-Object -ExpandProperty LogName | ForEach-Object { $_.split("-")[1] } | Sort-Object -Unique
}

Register-PSFTeppScriptblock -Name "WinEventLogCustomization.FolderThirdLevel" -ScriptBlock {
    Get-WinEvent -ListLog *-*-* -ErrorAction Ignore | Select-Object -ExpandProperty LogName | ForEach-Object { $_.split("-")[2].split("/")[0] } | Sort-Object -Unique
}

Register-PSFTeppScriptblock -Name "WinEventLogCustomization.ChannelName" -ScriptBlock {
    Get-WinEvent -ListLog */* -ErrorAction Ignore | Select-Object -ExpandProperty LogName | ForEach-Object { $_.split("/")[1] } | Sort-Object -Unique
}


# Get-WELCEventChannel
Register-PSFTeppArgumentCompleter -Command Get-WELCEventChannel -Parameter "ChannelFullName" -Name "WinEventLogCustomization.ChannelFullName"

# Set-WELCEventChannel
Register-PSFTeppArgumentCompleter -Command Set-WELCEventChannel -Parameter "ChannelFullName" -Name "WinEventLogCustomization.ChannelFullName"
Register-PSFTeppArgumentCompleter -Command Set-WELCEventChannel -Parameter "Enabled" -Name "WinEventLogCustomization.Bool"
Register-PSFTeppArgumentCompleter -Command Set-WELCEventChannel -Parameter "CompressLogFolder" -Name "WinEventLogCustomization.Bool"
Register-PSFTeppArgumentCompleter -Command Set-WELCEventChannel -Parameter "AllowFileAccessForLocalService" -Name "WinEventLogCustomization.Bool"
Register-PSFTeppArgumentCompleter -Command Set-WELCEventChannel -Parameter "MaxEventLogSize" -Name "WinEventLogCustomization.MaxEventLogSize"

# New-WELCEventChannelManifest
Register-PSFTeppArgumentCompleter -Command New-WELCEventChannelManifest -Parameter "FolderRoot" -Name "WinEventLogCustomization.FolderRoot"
Register-PSFTeppArgumentCompleter -Command New-WELCEventChannelManifest -Parameter "FolderSecondLevel" -Name "WinEventLogCustomization.FolderSecondLevel"
Register-PSFTeppArgumentCompleter -Command New-WELCEventChannelManifest -Parameter "FolderThirdLevel" -Name "WinEventLogCustomization.FolderThirdLevel"
Register-PSFTeppArgumentCompleter -Command New-WELCEventChannelManifest -Parameter "ChannelName" -Name "WinEventLogCustomization.ChannelName"


New-PSFLicense -Product 'WinEventLogCustomization' -Manufacturer 'Andreas Bellstedt' -ProductVersion $script:ModuleVersion -ProductType Module -Name MIT -Version "1.0.0.0" -Date (Get-Date "2022-06-26") -Text @"
Copyright (c) 2022 Andreas Bellstedt
 
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.
"@

#endregion Load compiled code