3LToolKit.psm1

Function Get-3LLogonEventAccountName {

    #requires -RunAsAdministrator

    #requires -Modules PSScriptTools

    <#
 
    .SYNOPSIS
        Get all logon events (Success and Failure) and extract some logon details
 
    .SYNOPSIS
        Get all authenticate events that are used to authenticate.
        Logon attempts are retrieved from events in the security eventlog with eventid 4624 and 4625.
        Each computer is queried as a separate job. The output of all jobs is merged in the end.
 
    .EXAMPLE
        Get-3LLogonEventAccountName -InformationAction Continue
         
        Get all logon events on the local machine and displays status messages.
 
    .EXAMPLE
        Get-3LLogonEventAccountName -afterDate (Get-Date).addDays(-14)
 
        Get all logon events on the local machine in the last 14 days
 
    .EXAMPLE
        Get-3LLogonEventAccountName -afterDate (Get-Date).addHours(-1)
 
        Get all logon events on the local machine in the hour
 
    .EXAMPLE
        Get-3LLogonEventAccountName -computerName SVR1, SVR2 -Progress
 
        Get all logon events on the specified computers.
        During analyzing the Security log Events, a progressbar is displayed.
         
 
    .EXAMPLE
        Get-3LLogonEventAccountName -InformationVariable info
        $info | Where Tags -eq 'Success'
 
        The first command collect all logon events on the local computer.
        Messages from the Information stream are collected in the info variable
        The second command shows all Information messages tagged as 'Success'
 
 
    .NOTES
 
        Author: Frits van Drie (3-Link.nl)
        Date : 2021-10-20
     
    #>


    [CmdletBinding()]

    param(

        [string[]]$computerName,
        [datetime]$afterDate = (Get-Date).AddDays(-1),
        [switch]$progress

    )

    Begin {

        Function Write--JobProgress {

            <#
                .NOTES
                    Date: 2021-10-19
            #>


            param($job)

            # Make sure the first child job exists
            if($job.ChildJobs[0].Progress -ne $null) {

                # Extract the latest progress of the job and write the progress
                $jobProgressHistory    = $job.ChildJobs[0].Progress
                $latestProgress        = $jobProgressHistory[$jobProgressHistory.Count - 1]
                $latestPercentComplete = $latestProgress | Select -expand PercentComplete
                $latestActivity        = $latestProgress | Select -expand Activity
                $latestStatus          = $latestProgress | Select -expand StatusDescription

                # a unique ID per ProgressBar

                if ( $latestProgress.PercentComplete -ge 0 ) {

                    Write-Progress -Id $job.Id -Activity $latestActivity -Status $latestStatus -PercentComplete $latestPercentComplete

                }
            }
        }

        # module PSScriptTools required
        try { 
            $module = 'PSScriptTools'
            Import-Module $module -ErrorAction Stop
            Write-Information "SUCCESS: Imported Module: $module"

            $function_ConvertEventLogRecord = "Function Convert-EventLogRecord {" + (Get-Item function:Convert-EventLogRecord).ScriptBlock + " }"

        }
        catch {
            Write-Information "FAILURE: Could not import required Module: $module"
            Write-Error $_
            Break
        }

        if ( ($PSBoundParameters.Keys -notcontains 'computerName') -or ($computerName -eq 'localhost') ) {
            $computerName = $env:COMPUTERNAME
        }

        $msec = ((Get-Date)- $afterDate).TotalMilliseconds

        $xml  = @"
        <QueryList>
          <Query Id="0" Path="Security">
            <Select Path="Security">*[System[(Level=2 or Level=4 or Level=0) and (EventID=4624 or EventID=4625) and (TimeCreated[timediff(@SystemTime) `&lt;= $msec])]]</Select>
          </Query>
        </QueryList>
"@
 # xmlFilter

        [array]$startedJobs = @()


    } # end Begin{}

    Process {

        foreach ( $computer in $computerName ) {

            $jobName = "LogonEvents-$computer"
            Write-Information "STATUS : Running Job $jobName" -Tags 'Status'
            $job = Start-Job -Name $jobName -ArgumentList $xml, $computer, $progress, $function_ConvertEventLogRecord {

                param($xml, $computer, $progress, $function_ConvertEventLogRecord)

                Write-Information "STATUS : Setting up session to computer $computer" -Tags 'Status'

                try {
                    New-PSSession -ComputerName $computer -ErrorAction Stop
                    Write-Information "SUCCESS: Connected to computer $computer" -Tags 'Success'
                } # try New-PSSession
                catch {
                    Write-Information "FAILURE: Session to computer $computer failed" -Tags 'Failure'
                    continue

                }

                if ( $session = Get-PSSession -ComputerName $computer -ErrorAction SilentlyContinue) {
                    Write-Information "STATUS : Starting Invoke-Command to computer $computer" -Tags 'Status'
                    $outputInvoke = Invoke-Command -Session $session -ArgumentList $xml, $progress, $activity, $function_ConvertEventLogRecord {

                        param($xml, $progress, $activity, $function_ConvertEventLogRecord)

                        iex $function_ConvertEventLogRecord  # load function: Convert-EventLogRecord

                        $computer = $env:COMPUTERNAME

                        $splatParam = @{}
                        $splatParam.add('FilterXml', $xml)
                        $splatParam.add('ComputerName', $computer)

                        try {

                            Write-Information "STATUS : Reading events from computer $computer" -Tags 'Status'
                            $events = Get-WinEvent @splatParam -ErrorAction Stop

                            $total  = $events.Count

                        }
                        catch {
                            Write-Information "INFO : $($_.tostring())"
                            Write-Error $_
                            return
                        }


                        $activity = "Analyzing $total Events from computer $computer"
                        Write-Information "STATUS : $activity" -Tags 'Status'

                        $done = 0

                        $returnInvoke  = @()

                        foreach ($event in $events) {

                            $obj = Convert-EventLogRecord $event

                            $returnInvoke += $obj

                            if ( $progress ) {
                                $done++
                                Write-Progress -Activity $activity -Status "$done\$total" -PercentComplete ($done/$total*100)
                            }

                        } # end foreach event

                        if ( $progress ) {
                            Write-Progress -Activity $activity -Status "$done\$total" -PercentComplete ($done/$total*100)
                            #Write-Progress -Activity $activity -Completed
                        }


                        return $returnInvoke


                    }
                    # end Invoke-Command


                    Get-PSSession -ComputerName $computer | Remove-PSSession

                    return $outputInvoke

                } # end if Get-PSSession

            } # end Start-Job

            [array]$startedJobs += $job

        } # end foreach computer
        Write-Information ""

    } # end Process{}


    End {

# while ( $jobs = (Get-Job -Name '*-LogonEvents') ) {
        Write-Information "STATUS : Waiting for jobs to finish`n"

        while ( $startedJobs ) {
# foreach ( $job in $jobs ) {
            foreach ( $job in $startedJobs ) {

                $jobState = $job.State  # to prevent status changes during processing

                if ($jobState -eq 'Completed' ) {

                    Write--JobProgress -job $job

                    Write-Information "STATUS : Importing results from job: $(($job).Name)" -Tags 'Status'
                     $receivedJob = Receive-Job $job

                    $job | Remove-Job
                    $startedJobs = $startedJobs | Where {$_ -ne $job}

                    if ($receivedJob.length -gt 1) {
                        # remove the first item which is the job
                        [array]$outputJob += $receivedJob[1..($receivedJob.length-1)]
                    }

                    Write-Information "SUCCESS: Job ready: $(($job).Name)`n" -Tags 'Success'
                }
                else {
                    Write--JobProgress -job $job
                    #sleep 1
                }


            } # end foreach job
            
        } # end While



        Write-Output $outputJob

    } # end End{}
        
}

Function Write-3LNetworkConfigurationToFile {

<#
 
.SYNOPSIS
    Find text in a file
 
.NOTES
    Author : Frits van Drie (3-Link.nl)
    Versions : 2021.09.24
 
.EXAMPLE
    SaveNetworkConfigurationToFile -openFile
 
 
.EXAMPLE
    SaveNetworkConfigurationToFile -path 'C:\data\network.txt'
 
#>



    [CmdletBinding()]

    param (

        [string]$path = "$($env:USERPROFILE)\Documents\$($env:COMPUTERNAME)-$($MyInvocation.MyCommand)-$(Get-Date -Format yyyyMMdd_HHmmss).txt",

        [string]$header,

        [switch]$openFile,

        [switch]$noFile

    )

    [array]$result = @()
    $result += "================================================================================================="
    $result += "Computer : $($env:COMPUTERNAME)"
    $result += "Date : $(Get-Date -Format 'yy-MM-dd (HH:mm:ss)')"
    if ($MyInvocation.BoundParameters.ContainsKey('header')) {
    
        $result += "Description: $header"
    }
    $result += "=================================================================================================`n"
    

    # Gather info
    try {

        $result += "`n================================================================================================="
        $result += "Get-NetAdapter"
        $result += "=================================================================================================`n"
        $result += Get-NetAdapter              -ErrorAction Stop | ft -AutoSize


        $result += "`n================================================================================================="
        $result += "Get-NetIPAddress"
        $result += "=================================================================================================`n"
        $result += Get-NetIPAddress            -ErrorAction Stop | ft -AutoSize


        $result += "`n================================================================================================="
        $result += "Get-NetRoute"
        $result += "=================================================================================================`n"
        $result += Get-NetRoute                -ErrorAction Stop | ft -AutoSize


        $result += "`n================================================================================================="
        $result += "Get-DnsClientCache"
        $result += "=================================================================================================`n"
        $result += Get-DnsClientCache          -ErrorAction Stop | ft -AutoSize


        $result += "`n================================================================================================="
        $result += "Show-DnsServerCache"
        $result += "=================================================================================================`n"
        (Get-DnsClientServerAddress -Family IPv4 | Where serveraddresses ).serveraddresses | foreach {
            $result += "`n===================="
            $result += "DNS: $_"
            $result += "====================`n"
            try {
                $result += Show-DnsServerCache -ComputerName $_ -ea Stop
            }
            catch {
                $result += "Cache of $_ could not be read"
            }
        }


        $result += "`n================================================================================================="
        $result += "Get-NetTCPConnection"
        $result += "=================================================================================================`n"
        $result += Get-NetTCPConnection -ErrorAction Stop | ft -AutoSize


        $result += "`n================================================================================================="
        $result += "Get-Process"
        $result += "=================================================================================================`n"
        $result += Get-Process -ErrorAction Stop | ft -AutoSize


        $result += "`n================================================================================================="
        $result += "Get-DnsClientServerAddress"
        $result += "=================================================================================================`n"
        $result += Get-DnsClientServerAddress  -ErrorAction Stop | ft -AutoSize


        $result += "`n================================================================================================="
        $result += "Certificates"
        $result += "=================================================================================================`n"
        $result += GCI 'Cert:\LocalMachine\My' -ErrorAction Stop | ft -AutoSize
        $result += GCI 'Cert:\CurrentUser\My'  -ErrorAction Stop | ft -AutoSize

    }

    catch {

        Write-Error $error[0]
        break

    }


    if ( $noFile ) {

        $openFile = $true

        $path = Join-Path $env:TEMP (Split-Path $path -Leaf)

    }



    try {

        $result | Out-File $path -Append -ErrorAction Stop -Width 200

    }

    catch {

        Write-Error $error[0]
        break

    }



    if ($openFile) {

        notepad.exe $path

    }
    


    if ($noFile) {

        Write-Verbose "Network configuration saved to $path"

        Start-Sleep 2

        Remove-Item $path -Force -ea SilentlyContinue

    }

    else {

        Write-Host "Network configuration saved to $path" -f Yellow

    }

}

Function Out-3LNotepad {
  param
  (
    [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
    [String]
    [AllowEmptyString()] 
    $Text
  )

  begin
  {
    $sb = New-Object System.Text.StringBuilder
  }

  process
  {
    $null = $sb.AppendLine($Text)
  }
  end
  {
    $text = $sb.ToString()

    $process = Start-Process notepad -PassThru
    $null = $process.WaitForInputIdle()


    $sig = '
      [DllImport("user32.dll", EntryPoint = "FindWindowEx")]public static extern IntPtr FindWindowEx(IntPtr hwndParent, IntPtr hwndChildAfter, string lpszClass, string lpszWindow);
      [DllImport("User32.dll")]public static extern int SendMessage(IntPtr hWnd, int uMsg, int wParam, string lParam);
    '


    $type = Add-Type -MemberDefinition $sig -Name APISendMessage -PassThru
    $hwnd = $process.MainWindowHandle
    [IntPtr]$child = $type::FindWindowEx($hwnd, [IntPtr]::Zero, "Edit", $null)
    $null = $type::SendMessage($child, 0x000C, 0, $text)
  }
}

Function Publish-3LScriptOrModule {

    <#
 
    .NOTES
        Author: Frits van Drie (3-Link.nl)
        Date : 2021-10-20
 
    .SYNOPSIS
        Publishes a script or module to a private or online repository
     
    .DESCRIPTION
        Publishes a script (ps1) or module (psm1) to a private or online repository {PSGallery).
        Before a module is published, a manifest file is created in the same folder. By default all functions in the module are exported.
        If an online repository (PSGallery) is used, a NuGet apiKey is needed.
 
    .PARAMETER ScriptPath
        Specifying this parameter will let you publish a script (only .ps1 extensions are valid)
 
    .PARAMETER ModulePath
        Specifying this parameter will let you publish a module (only .psm1 extensions are valid)
 
    .PARAMETER Repository
        This allows you to specify a private repository instead of the default PSGallery
 
    .PARAMETER GetSavedSettings
        Using this parameter settings are listed from a json-file in the user profile.
        Settings saved in this file: ApiKey, Author, Company, Copyright, Description, Repository
 
    .PARAMETER UseSavedSettings
        Using this parameter some parameters and values are retrieved from json-file in the user profile.
        When a parameter is also specified in the command, it will override that setting.
        Settings saved in this file: ApiKey, Author, Company, Copyright, Description, Repository
 
    .PARAMETER SaveSettings
        Use this parameter to save some settings to a json-file in the user profile.
        Settings saved in this file: ApiKey, Author, Company, Copyright, Description, Repository
 
    .PARAMETER FunctionsToExport
        This parameter let you specify the FunctionsToExport in the module-manifest file. By default all commands in the module-file are added
        Specifying the parameter will override the default.
 
    .PARAMETER Force
        Force can only be used when publishing a script.
        It allows you to overwrite an existing scriptpackage with the same version
 
    .EXAMPLE
        Publish-3LScriptOrModule -GetSavedSettings
        This lists the current settings in the json-file that is stored under AppData\Local
        If no file exists, a new file is created. Use the saveSettings parameter to update the default values.
 
    .EXAMPLE
        Publish-3LScriptOrModule -author $author -company $company -copyright $copyright -description $description -SaveSettings.
 
        This command publishes a module to a private repository. Some settings will be saved in a json-file in the user profile
 
    .EXAMPLE
        Publish-3LScriptOrModule `
            -author $author -company $company -copyright $copyright -description $description -repository 'PSGallery' -scriptPath $scriptpath -version 1.0.0 -Force
 
        This command publishes a script to the PSGallery. If the version is already present it will be overwritten (-Force).
 
    .EXAMPLE
        Publish-3LScriptOrModule -repository 'PSGallery' -scriptPath $scriptpath -version 1.0.0 -UseSavedSettings -verbose
 
        This command publishes a script to the PSGallery. Settings stored in json-file are used.
 
    .EXAMPLE
        Publish-3LScriptOrModule -saveSettings -author $author -company $company -copyright $copyright -description $description -repository $repository -apiKey $apiKey
        This creates a new json settingsfile in the user profile or overwrites an existing file
 
    .EXAMPLE
        Publish-3LScriptOrModule -saveSettings -author 'Joe Codex'
        This updates the json settingsfile
 
    #>


    [CmdletBinding(DefaultParameterSetName="Module")]

    param(

        [parameter(ParameterSetName='Script', mandatory=$true, position=0)]
        [string]$scriptPath,

        [parameter(ParameterSetName='Module', mandatory=$true, position=0)]
        [string]$modulePath,

        [parameter(ParameterSetName='Script', mandatory=$false, position=1)]
        [parameter(ParameterSetName='Module', mandatory=$false, position=1)]
        [parameter(ParameterSetName='WriteSettings', mandatory=$false, position=1)]
        [string]$repository = 'PSGallery',

        [parameter(ParameterSetName='Script', mandatory=$false, position=2)]
        [parameter(ParameterSetName='Module', mandatory=$false, position=2)]
        [parameter(ParameterSetName='WriteSettings', mandatory=$false, position=2)]
        [string]$apiKey,

        [parameter(ParameterSetName='Script'  , mandatory=$false, position=3)]
        [parameter(ParameterSetName='Module'  , mandatory=$false, position=3)]
        [parameter(ParameterSetName='WriteSettings', mandatory=$false, position=3)]
        [string]$author,

        [parameter(ParameterSetName='Script', mandatory=$false, position=4)]
        [parameter(ParameterSetName='Module', mandatory=$false, position=4)]
        [parameter(ParameterSetName='WriteSettings', mandatory=$false)]
        [string]$description,

        [parameter(ParameterSetName='Script', mandatory=$false, position=5)]
        [parameter(ParameterSetName='Module', mandatory=$false, position=5)]
        [string]$version,

        [parameter(ParameterSetName='Script', mandatory=$false, position=6)]
        [parameter(ParameterSetName='Module', mandatory=$false, position=6)]
        [parameter(ParameterSetName='WriteSettings', mandatory=$false, position=6)]
        [string]$company,

        [parameter(ParameterSetName='Script', mandatory=$false, position=7)]
        [parameter(ParameterSetName='Module', mandatory=$false, position=7)]
        [parameter(ParameterSetName='WriteSettings', mandatory=$false, position=7)]
        [string]$copyright,

        [parameter(ParameterSetName='Module', mandatory=$false, position=8)]
        [array]$functionsToExport,

        [parameter(ParameterSetName='Script', mandatory=$false)]
        [Switch]$Force=$false,

        [parameter(ParameterSetName='ReadSettings', mandatory=$false)]
        [Switch]$GetSavedSettings=$false,

        [parameter(ParameterSetName='Script', mandatory=$false)]
        [parameter(ParameterSetName='Module', mandatory=$false)]
        [Switch]$UseSavedSettings=$false,

        [parameter(ParameterSetName='WriteSettings', mandatory=$false)]
        [Switch]$SaveSettings=$false

    )


    # Variables
    $settingsName     = $MyInvocation.MyCommand.Name
    $settingsFolder   = "$env:LOCALAPPDATA\3-Link\$settingsName"
    $settingsFile     = "$settingsName.json"
    $settingsPath     = "$settingsFolder\$settingsFile"

    Function ImportSettingsFromFile {

        if ( -not(Test-Path $settingsPath -ErrorAction SilentlyContinue) ) {
            Write-Verbose "FAILURE: Settingsfile not found: $settingsPath"

            try {
                Write-Verbose "INFO : Creating new file: $settingsPath"
                $objSettings = @{}
                $objSettings.add('apiKey'     , 'update this')
                $objSettings.add('author'     , 'update this')
                $objSettings.add('company'    , 'update this')
                $objSettings.add('copyright'  , 'update this')
                $objSettings.add('description', 'update this')
                $objSettings.add('repository' , 'PSGallery')

                New-Item (Split-Path $settingsPath -Parent) -ItemType Directory -Force
                $objSettings | ConvertTo-Json -ErrorAction Stop | Out-File $settingsPath -ErrorAction Stop
                Write-Verbose "SUCCESS: Created new file: $settingsPath"

            }
            catch {
            
                Write-Error $_
                break

            } #end try\catch

        } # end If
        Write-Verbose "SUCCESS: $settingsPath found"

        Write-Verbose "INFO : Importing Settings from $settingsPath"
        $settings    = Get-Content $settingsPath | ConvertFrom-Json
        Write-Verbose "SUCCESS: Settings imported"

        return $settings

    }


    # Import Settings
    if ( $UseSavedSettings -or $GetSavedSettings ) {

        $settings    = ImportSettingsFromFile

        if ( $GetSavedSettings ) {

            return $settings

        }

        if ( $PSBoundParameters.Keys -notcontains 'apiKey' ) {
            $apiKey      = $settings.apiKey
        }
        if ( $PSBoundParameters.Keys -notcontains 'author' ) {
            $author      = $settings.author
        }
        if ( $PSBoundParameters.Keys -notcontains 'company' ) {
            $company     = $settings.company
        }
        if ( $PSBoundParameters.Keys -notcontains 'copyright' ) {
            $copyright   = $settings.copyright
        }
        if ( $PSBoundParameters.Keys -notcontains 'description' ) {
            $description = $settings.description
        }
        if ( $PSBoundParameters.Keys -notcontains 'repository' ) {
            $repository  = $settings.repository
        }

    }



    # Save Settings
    if ( $SaveSettings ) {

        $settings    = ImportSettingsFromFile

        if ( $PSBoundParameters.Keys -contains 'apiKey' ) {
            $settings.apiKey      = $apiKey
        }
        if ( $PSBoundParameters.Keys -contains 'author' ) {
            $settings.author      = $author
        }
        if ( $PSBoundParameters.Keys -contains 'company' ) {
            $settings.company     = $company
        }
        if ( $PSBoundParameters.Keys -contains 'copyright' ) {
            $settings.copyright   = $copyright
        }
        if ( $PSBoundParameters.Keys -contains 'description' ) {
            $settings.description = $description
        }
        if ( $PSBoundParameters.Keys -contains 'repository' ) {
            $settings.repository  = $repository
        }

        # Save settings
        try {
            Write-Verbose "INFO : Creating folder: $settingsFolder"
            $null = New-Item -Path $settingsFolder -ItemType Directory -Force -ErrorAction Stop
            Write-Verbose "SUCCESS: Created folder: $settingsFolder"

            Write-Verbose "INFO : Saving settings to $settingsPath"
            $settings | ConvertTo-Json | Out-File $settingsPath -ErrorAction Stop
            Write-Verbose "SUCCESS: Saved settings to $settingsPath"

            return $settings

        }
        catch {

            Write-Error $_

            return

        }

    } # end SaveSettings



    # FilePath
    if ( $PSBoundParameters.Keys -contains 'scriptPath') {
        $filePath = $scriptPath
    }

    elseif ( $PSBoundParameters.Keys -contains 'modulePath') {
        $filePath = $modulePath
    }

    try {

        if ( -not (Test-Path $filePath -ErrorAction Stop) ) {

            Throw

        }
        else {
            Write-Verbose "SUCCESS: File exists"
        }

    }
    catch {

        Write-Error "$filePath is not a valid file"

        return

    } # end try\catch



    # Repository
    if ( $repository -in (Get-PSRepository).Name ) {

        Write-Verbose "SUCCESS: $repository exists"

        # Repo trusted
        if ( (Get-PSRepository -Name $repository).InstallationPolicy -ne 'Trusted' ) {

            Write-Error "$repository is not a trusted repository"

            return

        }
        else {

            Write-Verbose "SUCCESS: $repository is trusted"

        }
    }
    else {

        Write-Error "$repository is not a valid repository"

        return

    }



    # PowerShellGet
    Write-Verbose "TEST : Test PowerShellGet version"
    if ( (Get-Module -Name PowerShellGet -ListAvailable).Version -lt 2.2.5 ) {

        Write-Error "PowerShellGet version 2.2.5 is required"
        break

    }
    Write-Verbose "SUCCESS: PowerShellGet requirements confirmed"



    # TLS 12
    Write-Verbose "TEST : TLS version min. Tls12 "
    if ( ([System.Net.ServicePointManager]::SecurityProtocol).value__ -lt 3072 ) {

        #Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols'
        Write-Error "Tls12 or newer is required. Use `'[System.Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12`' to enable Tls12"


        break

    }
    Write-Verbose "SUCCESS: TLS requirements confirmed"



    # script or module
    Write-Verbose "TEST : File extension"
    if ( $PSBoundParameters.Keys -contains 'ScriptPath') {

        if ( (Get-Item $filePath).Extension -eq '.ps1' ){

            $scriptName = (Split-Path $filePath -Leaf).Split('.')[0]
            Write-Verbose "SUCCESS: Valid script: $filePath"

        }

        else {

            Write-Error "Fileformat not supported: $filePath (only .ps1 is a valid extension)"
            return

        }

    } # end ScriptPath

    if ( $PSBoundParameters.Keys -contains 'ModulePath') {

        if ( (Get-Item $filePath).Extension -eq '.psm1' ){

            $moduleName = (Split-Path $filePath -Leaf).Split('.')[0]
            Write-Verbose "SUCCESS: Valid module: $filePath"

        }

        else {

            Write-Error "Fileformat not supported: $filePath (only .psm1 is a valid extension)"
            return

        }

    } # end ModulePath



    # is repository private or online
    Write-Verbose "TEST : Is repository Local or Online"
    if ( $repoIsLocal = Test-Path (Get-PSRepository $repository).sourcelocation ) {

        Write-Verbose "INFO : $repository is a private repository"

    }  # local Repo
    else {

        Write-Verbose "INFO : $repository is an online repository"

        # Validate apiKey
        if ( $apiKey -notmatch "^[a-z0-9]{46}$" ) {

            Write-Error "Not a valid ApiKey: $apiKey"
            return
        }
        Write-Verbose "SUCCESS: APIKey $apiKey has a valid format"

    } # online Repo



    # Publish Script
    if ($PSBoundParameters.Keys -contains 'ScriptPath') {

        $splatScript = @{}
        $splatScript.Add('Path', $filePath)
        $splatScript.Add('Repository', $repository)
            
        if ( -not($repoIsLocal) ) {

            $splatScript.Add('NuGetApiKey', $apiKey)

        }

        try {

            Write-Verbose "INFO : Publishing $filePath to $repository"
            Write-Verbose "INFO : Parameter Force: $Force"

            $publishedItem = Publish-Script @splatScript -ErrorAction Stop -Force:$Force

            $publishedItem = Find-Script -Name $LOCAL:scriptName -Repository $repository -ErrorAction Stop

            Write-Verbose "SUCCESS: Publishing $filePath to $repository successfull"

            Write-Output $publishedItem

        }

        catch {

            Write-Error $_

            return

        } # end try\catch

    } # end publish Script



    # Publish Module
    if ($PSBoundParameters.Keys -contains 'ModulePath') {


        if ( $PSBoundParameters.Keys -notcontains 'functionsToExport' ) {

            [array]$functionsToExport =  (Import-Module $modulePath -PassThru).ExportedCommands.keys

        }


        try {
            Write-Verbose "INFO : Creating Manifest $manifestPath"
            $manifestPath = $filePath.replace('psm1','psd1')

            $manifestFile = New-ModuleManifest `
                -Path              $manifestPath `
                -Author            $author `
                -CompanyName       $company `
                -Copyright         $copyright `
                -RootModule        $moduleName `
                -Description       $description `
                -ModuleVersion     $version `
                -FunctionsToExport $functionsToExport `
                -PassThru `
                -ErrorAction       Stop
            Write-Verbose "SUCCESS: Manifest created"


            Write-Verbose "INFO : Testing Manifest $manifestPath"
            $null = Test-ModuleManifest `
                -Path $modulePath.replace('psm1','psd1') `
                -ErrorAction Stop
            Write-Verbose "SUCCESS: Manifest tested"

        } # try New-ModuleManifest
        catch {

            Write-Error $_
            
            Break

        } # end try\catch


        try {

            Write-Verbose "INFO : Publishing $filePath to $repository"
            Write-Verbose "INFO : Parameter Force: $Force"

            $splatModule = @{}
            $splatModule.Add('Path'       , $moduleFolder)
            $splatModule.Add('Repository' , $repository)
            $splatModule.Add('Force'      , $Force)

            if ( $repository -eq 'PSGallery' ) {
                $splatModule.Add('NuGetApiKey', $apiKey)
            }


            $publishedItem = Publish-Module @splatModule -ErrorAction Stop #-WhatIf

            $publishedItem = Find-Module $LOCAL:moduleName -Repository $repository -ErrorAction Stop

            Write-Verbose "SUCCESS: Publishing $filePath to $repository successfull"
                
            Write-Output $publishedItem

        }   # try Publish-Module

        catch {

            $Error[0]

            Write-Error $_

            return

        } # end try\catch

    } # end Publish Module



}

Function Find-3LStringInFile {

<#
 
.SYNOPSIS
    Find text in a file
 
.NOTES
    Author : Frits van Drie (3-Link.nl)
    Versions : 2021.09.23
 
.EXAMPLE
    FindStringInFile -pattern 'Project' -path "document.txt"
    searches document.txt in current folder for 'project' or 'Project'
 
.EXAMPLE
    FindStringInFile -pattern 'Project' -path "document.txt" -caseSensitive
    searches document.txt in current folder for 'Project'
 
.EXAMPLE
    gci "C:\Users\user\documents" -File | foreach { FindStringInFile -pattern 'project' -path $_.FullName }
    searches all users documents 'project' or 'Project'
 
#>



    [CmdletBinding()]

    param (

        [parameter(mandatory = $true)]
        [string]$pattern,

        [parameter(mandatory = $true)]
        [string]$path,

        [parameter(mandatory = $false)]
        [switch]$caseSensitive

    )

    begin {

        Write-Verbose "Start Function: $($MyInvocation.MyCommand)"

    }

    process {

        foreach ($file in $path) {

            try {

                $objfile = Get-item $path -ea Stop

                $result = $objFile | Select-String -Pattern $pattern -ea Stop -CaseSensitive:$caseSensitive

                Write-Output $result

            }

            catch {

                Write-Error "File not found: $path"

            }
        }

    }

    end {

        Write-Verbose "End Function: $($MyInvocation.MyCommand)"

    }

}