Carbon.Windows.psm1

# Copyright WebMD Health Services
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License

#Requires -Version 5.1
Set-StrictMode -Version 'Latest'

# Functions should use $moduleRoot as the relative root from which to find
# things. A published module has its function appended to this file, while a
# module in development has its functions in the Functions directory.
$script:moduleRoot = $PSScriptRoot
$script:backConnHostNamesKeyPath = 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa\MSV1_0'
$script:backConnHostNamesValueName = 'BackConnectionHostNames'

$psModulesRoot = Join-Path -Path $script:moduleRoot -ChildPath 'Modules' -Resolve
Import-Module -Name (Join-Path -Path $psModulesRoot -ChildPath 'Carbon.Registry')

# Store each of your module's functions in its own file in the Functions
# directory. On the build server, your module's functions will be appended to
# this file, so only dot-source files that exist on the file system. This allows
# developers to work on a module without having to build it first. Grab all the
# functions that are in their own files.
$functionsPath = Join-Path -Path $script:moduleRoot -ChildPath 'Functions\*.ps1'
if( (Test-Path -Path $functionsPath) )
{
    foreach( $functionPath in (Get-Item $functionsPath) )
    {
        . $functionPath.FullName
    }
}



function Get-CBackConnectionHostName
{
    <#
    .SYNOPSIS
    Gets the back connection hostnames configured for the local computer.
 
    .DESCRIPTION
    The `Get-CBackConnectionHostName` function gets the current list of configured back connection hostnames.
 
    .EXAMPLE
    Get-CBackConnectionHostName
 
    Demonstrates how to get the back connection hostnames.
    #>

    [CmdletBinding()]
    param(
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    return Get-CRegistryKeyValue -Path $script:backConnHostNamesKeyPath -Name $script:backConnHostNamesValueName
}


function Get-CEventLog
{
    <#
    .SYNOPSIS
    Gets logs from Windows Event Viewer.
 
    .DESCRIPTION
    The `Get-CEventLog` function gets logs from the Windows Event Viewer. Use the `List` parameter to list all of the
    available event logs. Use the `LogName` parameter to get all of the available event log entries in a specific log.
    If the log doesn't exist, an error will be written.
 
    The `Newest` parameter can be used to get the latest logs. The `EntryType` parameter can be used to filter by the
    type of log entry. The `Message` parameter can be used to filter by the message of the log entry by matching the
    text within the log.
 
    .EXAMPLE
    Get-CEventLog -List
 
    Demonstrates listing all of the available Windows Event Logs.
 
    .EXAMPLE
    Get-CEventLog -LogName 'Application'
 
    Demonstrates getting all of the available event logs in the 'Application' log.
 
    .EXAMPLE
    Get-CEventLog -LogName 'System' -Newest 5
 
    Demonstrates getting the latest 5 logs from the 'System' event logs.
 
    .EXAMPLE
    Get-CEventLog -LogName 'Security' -EntryType 'Error' -Message '*SQL*'
 
    Demonstrates getting the event logs from the 'Security' log that have are of type 'Error' and contain 'SQL' in the
    message.
    #>

    [CmdletBinding(DefaultParameterSetName='list')]
    param(
        # List all event log categories.
        [Parameter(Mandatory, ParameterSetName='list')]
        [switch] $List,

        # The name of the log to view.
        [Parameter(Mandatory, ParameterSetName='logs')]
        [String] $LogName,

        # The number of latest logs to return.
        [Parameter(ParameterSetName='logs')]
        [int] $Newest,

        # The entry type to filter for.
        [Parameter(ParameterSetName='logs')]
        [Diagnostics.EventLogEntryType] $EntryType,

        # The message to filter for.
        [Parameter(ParameterSetName='logs')]
        [String] $Message
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    if ($List)
    {
        return [Diagnostics.EventLog]::GetEventLogs()
    }

    if ($LogName)
    {
        if (-not (Test-CEventLog -LogName $LogName))
        {
            $msg = "Failed to get log messages from ${LogName} event log because that event log doesn't exist."
            Write-Error -Message $msg -ErrorAction $ErrorActionPreference
            return
        }
        $log = [Diagnostics.EventLog]::New($LogName)

        $entries = $log.Entries

        if ($EntryType)
        {
            $entries = $entries | Where-Object EntryType -Like $EntryType
        }

        if ($Message)
        {
            $entries = $entries | Where-Object Message -Like $Message
        }

        if ($Newest)
        {
            $entries = $entries | Select-Object -Last $Newest
        }

        return $entries
    }
}


function Install-CEventLog
{
    <#
    .SYNOPSIS
    Creates an event log if it does not exist.
 
    .DESCRIPTION
    The `Install-CEventLog` function creates an event log on the local computer. If the event log already exists, this
    function does nothing.
 
    .EXAMPLE
    Install-CEventLog -LogName 'TestApp' -Source 'TestLog'
 
    Demonstrates creating a `TestLog` event log and registers `TestApp` as a source for the log.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        # The name of the log to be created.
        [Parameter(Mandatory)]
        [String] $LogName,

        # The source of the log to be created.
        [Parameter(Mandatory)]
        [String[]] $Source
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    $logExists = Test-CEventLog -LogName $LogName

    $missingSources = $Source | Where-Object { -not (Test-CEventLog -Source $_) }

    if ($logExists -and -not $missingSources)
    {
        return
    }

    New-CEventLog -LogName $LogName -Source $missingSources
}


function New-CEventLog
{
    <#
    .SYNOPSIS
    Creates an event log.
 
    .DESCRIPTION
    The `New-CEventLog` creates a new Windows Event Log on the local computer and registers event sources for the log.
 
    This function will write an error if any of the event source already exists. No changes will be made to the system.
 
    .EXAMPLE
    New-CEventLog -LogName 'TestApp' -Source 'TestLog'
 
    Demonstrates creating a `TestLog` event log and registers `TestApp` as a source for the log.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        # The name of the log to be created.
        [Parameter(Mandatory)]
        [String] $LogName,

        # The source of the events to be written to the log.
        [Parameter(Mandatory)]
        [String[]] $Source
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    foreach ($s in $Source)
    {
        if (Test-CEventLog -Source $s)
        {
            Write-Error -Message "Event log source '${s}' already exists." -ErrorAction $ErrorActionPreference
            return
        }
    }

    foreach ($s in $Source)
    {
        if ($PSCmdlet.ShouldProcess("$s event log source '${LogName}'", "install"))
        {
            [Diagnostics.EventLog]::CreateEventSource($s, $LogName)
        }
    }
}


function Register-CBackConnectionHostName
{
    <#
    .SYNOPSIS
    Adds hostnames to the back connection hostname list.
 
    .DESCRIPTION
    The `Register-CBackConnectionHostName` function adds a hostname to the list of back connection hostnames. If the
    hostname is already in the list, it does nothing. You can pass a single hostname to the `HostName` parameter or pipe
    in multiple hostnames.
 
    .EXAMPLE
    Register-CBackConnectionHostName -HostName 'example.com'
 
    Demonstrates how to add an item to the back connection hostnames list by passing a single hostname to the `HostName`
    parameter.
 
    .EXAMPLE
    'example.com', 'example2.com' | Register-CBackConnectionHostName
 
    Demonstrates how to add multiple hostnames to the back connction hostnames list by pipeling them to the
    `Register-CBackConnectionHostName` function.
    #>

    [CmdletBinding()]
    param(
        # The hostname to add to the list.
        [Parameter(Mandatory, ValueFromPipeline)]
        [String] $HostName
    )

    begin
    {
        Set-StrictMode -Version 'Latest'
        Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

        [String[]] $curHostNames = Get-CBackConnectionHostName
        $hostnamesToAdd = [Collections.Generic.List[String]]::New()
    }

    process
    {

        if ($curHostNames -contains $HostName)
        {
            return
        }

        [void]$hostnamesToAdd.Add($HostName)
    }

    end
    {
        if ($hostnamesToAdd.Count -eq 0)
        {
            return
        }

        $newValue = & {
            if ($curHostNames)
            {
                $curHostNames | Write-Output
            }
            $hostnamesToAdd | Select-Object -Unique | Write-Output
        }

        Set-CRegistryKeyValue -Path $script:backConnHostNamesKeyPath `
                              -Name $script:backConnHostNamesValueName `
                              -Strings $newValue
    }
}


function Remove-CEventLog
{
    <#
    .SYNOPSIS
    Removes an event log.
 
    .DESCRIPTION
    The `Remove-CEventLog` function removes an event log from the local computer and unregisters all of the associated
    event sources. If the event log does not exist, an error is written. If the `Source` parameter is specified, only
    the provided sources will be unregistered. The log associated with the sources will not be removed. If the source
    does not exist, an error is thrown.
 
    .EXAMPLE
    Remove-CEventLog -LogName 'TestApp'
 
    Demonstrates removing the `TestApp` event log.
 
    .EXAMPLE
    Remove-CEventLog -Source 'TestLog'
 
    Demonstrates unregistering the `TestLog` event source regardless of the Log it belongs to.
    #>

    [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName='LogName')]
    param(
        # The name of the log to remove.
        [Parameter(Mandatory, ParameterSetName='LogName')]
        [String[]] $LogName,

        # The name of the event source to remove.
        [Parameter(Mandatory, ParameterSetName='Source')]
        [String[]] $Source
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    if ($LogName)
    {
        foreach ($log in $LogName)
        {
            if (-not (Test-CEventLog -LogName $log))
            {
                Write-Error -Message "Event log '${LogName}' does not exist." -ErrorAction $ErrorActionPreference
                return
            }

            if ($PSCmdlet.ShouldProcess("event log '$LogName'", "delete"))
            {
                [Diagnostics.EventLog]::Delete($LogName)
            }
        }
        return
    }

    foreach ($s in $Source)
    {
        if (-not (Test-CEventLog -Source $s))
        {
            Write-Error -Message "Event log source '${s}' does not exist." -ErrorAction $ErrorActionPreference
            return
        }

        if ($PSCmdlet.ShouldProcess("event log source '${s}'", "uninstall"))
        {
            [Diagnostics.EventLog]::DeleteEventSource($s)
        }
    }
}


function Test-CEventLog
{
    <#
    .SYNOPSIS
    Tests if an event log exists.
 
    .DESCRIPTION
    The `Test-CEventLog` function checks to see if an event log or event source exists. Use the `LogName` parameter to
    test if an event log exists. Use the `Source` parameter to test if an event source exists.
 
    .EXAMPLE
    Test-CEventLog -LogName 'TestApp'
 
    Demonstrates testing if the `TestApp` event log exists.
 
    .EXAMPLE
    Test-CEventLog -Source 'TestLog'
 
    Demonstrate testing if the `TestLog` event source exists.
    #>

    [CmdletBinding(DefaultParameterSetName='LogName')]
    param(
        [Parameter(Mandatory, ParameterSetName='LogName')]
        [String] $LogName,

        [Parameter(Mandatory, ParameterSetName='Source')]
        [String] $Source
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    if ($LogName)
    {
        return [Diagnostics.EventLog]::Exists($LogName)
    }
    return [Diagnostics.EventLog]::SourceExists($Source)
}


function Uninstall-CEventLog
{
    <#
    .SYNOPSIS
    Removes an event log if it exists.
 
    .DESCRIPTION
    The Uninstall-CEventLog function removes an event log from the local computer, if it exists. If the event log
    doesn't exist, the function does nothing.
 
    .EXAMPLE
    Uninstall-CEventLog -LogName 'TestApp'
 
    Demonstrates removing the `TestApp` event log.
    #>

    [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName='LogName')]
    param(
        [Parameter(Mandatory, ParameterSetName='LogName')]
        [String[]] $LogName,

        [Parameter(Mandatory, ParameterSetName='Source')]
        [String[]] $Source
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    if ($LogName)
    {
        foreach ($log in $LogName)
        {
            if (Test-CEventLog -LogName $log)
            {
                Remove-CEventLog -LogName $log
            }
        }
        return
    }

    foreach ($s in $source)
    {
        if (Test-CEventLog -Source $s)
        {
            Remove-CEventLog -Source $s
        }
    }
}


function Unregister-CBackConnectionHostName
{
    <#
    .SYNOPSIS
    Removes hostnames from the back connection hostname list.
 
    .DESCRIPTION
    The `Unregister-CBackConnectionHostName` function removes a hostname from the list of back connection hostnames. If
    the hostname is not in the list, it does nothing. You can pass a single hostname to the `HostName` parameter or pipe
    in multiple hostnames.
 
    .EXAMPLE
    Unregister-CBackConnectionHostName -HostName 'example.com'
 
    Demonstrates how to remove an item from the back connection hostnames list by passing a single hostname to the
    `HostName` parameter.
 
    .EXAMPLE
    'example.com', 'example2.com' | Unregister-CBackConnectionHostName
 
    Demonstrates how to remove multiple hostnames from the back connction hostnames list by pipeling them to the
    `Register-CBackConnectionHostName` function.
    #>

    [CmdletBinding()]
    param(
        # The hostname to remove from the back connection hostname list.
        [Parameter(Mandatory, ValueFromPipeline)]
        [String] $HostName
    )

    begin
    {
        Set-StrictMode -Version 'Latest'
        Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

        $hostnamesToRemove = [Collections.Generic.List[String]]::New()
    }

    process
    {
        [void]$hostnamesToRemove.Add($HostName)
    }

    end
    {
        [String[]] $currentValues = Get-CBackConnectionHostName
        if ($null -eq $currentValues)
        {
            $currentValues = @()
        }

        [String[]] $newValues = $currentValues | Where-Object { $_ -notin $hostnamesToRemove }
        if ($null -eq $newValues)
        {
            $newValues = @()
        }

        if ($newValues.Count -eq $currentValues.Count)
        {
            return
        }

        Set-CRegistryKeyValue -Path $script:backConnHostNamesKeyPath `
                              -Name $script:backConnHostNamesValueName `
                              -Strings $newValues
    }
}


function Use-CallerPreference
{
    <#
    .SYNOPSIS
    Sets the PowerShell preference variables in a module's function based on the callers preferences.
 
    .DESCRIPTION
    Script module functions do not automatically inherit their caller's variables, including preferences set by common
    parameters. This means if you call a script with switches like `-Verbose` or `-WhatIf`, those that parameter don't
    get passed into any function that belongs to a module.
 
    When used in a module function, `Use-CallerPreference` will grab the value of these common parameters used by the
    function's caller:
 
     * ErrorAction
     * Debug
     * Confirm
     * InformationAction
     * Verbose
     * WarningAction
     * WhatIf
     
    This function should be used in a module's function to grab the caller's preference variables so the caller doesn't
    have to explicitly pass common parameters to the module function.
 
    This function is adapted from the [`Get-CallerPreference` function written by David Wyatt](https://gallery.technet.microsoft.com/scriptcenter/Inherit-Preference-82343b9d).
 
    There is currently a [bug in PowerShell](https://connect.microsoft.com/PowerShell/Feedback/Details/763621) that
    causes an error when `ErrorAction` is implicitly set to `Ignore`. If you use this function, you'll need to add
    explicit `-ErrorAction $ErrorActionPreference` to every `Write-Error` call. Please vote up this issue so it can get
    fixed.
 
    .LINK
    about_Preference_Variables
 
    .LINK
    about_CommonParameters
 
    .LINK
    https://gallery.technet.microsoft.com/scriptcenter/Inherit-Preference-82343b9d
 
    .LINK
    http://powershell.org/wp/2014/01/13/getting-your-script-module-functions-to-inherit-preference-variables-from-the-caller/
 
    .EXAMPLE
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
 
    Demonstrates how to set the caller's common parameter preference variables in a module function.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        #[Management.Automation.PSScriptCmdlet]
        # The module function's `$PSCmdlet` object. Requires the function be decorated with the `[CmdletBinding()]`
        # attribute.
        $Cmdlet,

        [Parameter(Mandatory)]
        # The module function's `$ExecutionContext.SessionState` object. Requires the function be decorated with the
        # `[CmdletBinding()]` attribute.
        #
        # Used to set variables in its callers' scope, even if that caller is in a different script module.
        [Management.Automation.SessionState]$SessionState
    )

    Set-StrictMode -Version 'Latest'

    # List of preference variables taken from the about_Preference_Variables and their common parameter name (taken
    # from about_CommonParameters).
    $commonPreferences = @{
                              'ErrorActionPreference' = 'ErrorAction';
                              'DebugPreference' = 'Debug';
                              'ConfirmPreference' = 'Confirm';
                              'InformationPreference' = 'InformationAction';
                              'VerbosePreference' = 'Verbose';
                              'WarningPreference' = 'WarningAction';
                              'WhatIfPreference' = 'WhatIf';
                          }

    foreach( $prefName in $commonPreferences.Keys )
    {
        $parameterName = $commonPreferences[$prefName]

        # Don't do anything if the parameter was passed in.
        if( $Cmdlet.MyInvocation.BoundParameters.ContainsKey($parameterName) )
        {
            continue
        }

        $variable = $Cmdlet.SessionState.PSVariable.Get($prefName)
        # Don't do anything if caller didn't use a common parameter.
        if( -not $variable )
        {
            continue
        }

        if( $SessionState -eq $ExecutionContext.SessionState )
        {
            Set-Variable -Scope 1 -Name $variable.Name -Value $variable.Value -Force -Confirm:$false -WhatIf:$false
        }
        else
        {
            $SessionState.PSVariable.Set($variable.Name, $variable.Value)
        }
    }
}