SCOrchDev-Utility.psm1

#requires -Version 2
<#
.SYNOPSIS
    Converts an object into a text-based represenation that can easily be written to logs.
 
.DESCRIPTION
    Format-ObjectDump takes any object as input and converts it to a text string with the
    name and value of all properties the object's type information. If the property parameter
    is supplied, only the listed properties will be included in the output.
 
.PARAMETER InputObject
    The object to convert to a textual representation.
 
.PARAMETER Property
    An optional list of property names that should be displayed in the output!
#>

Function Format-ObjectDump
{
    [CmdletBinding()]
    [OutputType([string])]
    Param(
        [Parameter(Position = 0, Mandatory = $True,ValueFromPipeline = $True)]
        [Object]$InputObject,
        [Parameter(Position = 1, Mandatory = $False)] [string[]] $Property = @('*')
    )
    $typeInfo = $InputObject.GetType() | Out-String
    $objList = $InputObject | `
        Format-List -Property $Property | `
        Out-String

    return "$typeInfo`r`n$objList"
}

<#
.SYNOPSIS
    Converts an input string into a boolean value.
 
.DESCRIPTION
    $values = @($null, [String]::Empty, "True", "False",
                "true", "false", " true ", "0",
                "1", "-1", "-2", '2', "string", 'y', 'n'
                'yes', 'no', 't', 'f');
    foreach ($value in $values)
    {
        Write-Verbose -Message "[$($Value)] Evaluated as [`$$(ConvertTo-Boolean -InputString $value)]" -Verbose
    }
 
    VERBOSE: [] Evaluated as [$False]
    VERBOSE: [] Evaluated as [$False]
    VERBOSE: [True] Evaluated as [$True]
    VERBOSE: [False] Evaluated as [$False]
    VERBOSE: [true] Evaluated as [$True]
    VERBOSE: [false] Evaluated as [$False]
    VERBOSE: [ true ] Evaluated as [$True]
    VERBOSE: [0] Evaluated as [$False]
    VERBOSE: [1] Evaluated as [$True]
    VERBOSE: [-1] Evaluated as [$True]
    VERBOSE: [-2] Evaluated as [$True]
    VERBOSE: [2] Evaluated as [$True]
    VERBOSE: [string] Evaluated as [$True]
    VERBOSE: [y] Evaluated as [$True]
    VERBOSE: [n] Evaluated as [$False]
    VERBOSE: [yes] Evaluated as [$True]
    VERBOSE: [no] Evaluated as [$False]
    VERBOSE: [t] Evaluated as [$True]
    VERBOSE: [f] Evaluated as [$False]
 
.PARAMETER InputString
    The string value to convert
#>

Function ConvertTo-Boolean
{
    [OutputType([string])]
    Param(
        [Parameter(Mandatory = $True, ValueFromPipeline = $True)]
        [AllowNull()]
        [string]
        $InputString
    )

    if(-not [System.String]::IsNullOrEmpty($InputString))
    {
        $res    = $True
        $success = [bool]::TryParse($InputString,[ref]$res)
        if($success)
        {
            return $res
        }
        else
        {
            $InputString = ([string]$InputString).ToLower()
    
            Switch ($InputString)
            {
                'f'     
                {
                    $False 
                }
                'false' 
                {
                    $False 
                }
                'off'   
                {
                    $False 
                }
                'no'    
                {
                    $False 
                }
                'n'     
                {
                    $False 
                }
                default
                {
                    try
                    {
                        return [bool]([int]$InputString)
                    }
                    catch
                    {
                        return [bool]$InputString
                    }
                }
            }
        }
    }
    else
    {
        return $False
    }
}
<#
.SYNOPSIS
    Given a list of values, returns the first value that is valid according to $FilterScript.
 
.DESCRIPTION
    Select-FirstValid iterates over each value in the list $Value. Each value is passed to
    $FilterScript as $_. If $FilterScript returns true, the value is considered valid and
    will be returned if no other value has been already. If $FilterScript returns false,
    the value is deemed invalid and the next element in $Value is checked.
 
    If no elements in $Value are valid, returns $Null.
 
.PARAMETER Value
    A list of values to check for validity.
 
.PARAMETER FilterScript
    A script block that determines what values are valid. Elements of $Value can be referenced
    by $_. By default, values are simply converted to Bool.
#>

Function Select-FirstValid
{
    # Don't allow values from the pipeline. The pipeline does weird things with
    # nested arrays.
    Param(
        [Parameter(Mandatory = $True, ValueFromPipeline = $False)]
        [AllowNull()]
        $Value,

        [Parameter(Mandatory = $False)]
        $FilterScript = {
            $_ -As [Bool] 
        }
    )
    ForEach($_ in $Value)
    {
        If($FilterScript.InvokeWithContext($Null, (Get-Variable -Name '_'), $Null))
        {
            Return $_
        }
    }
    Return $Null
}

<#
.SYNOPSIS
    Returns a dictionary mapping the name of a PowerShell command to the file containing its
    definition.
 
.DESCRIPTION
    Find-DeclaredCommand searches $Path for .ps1 files. Each .ps1 is tokenized in order to
    determine what functions and workflows are defined in it. This information is used to
    return a dictionary mapping the command name to the file in which it is defined.
 
.PARAMETER Path
    The path to search for command definitions.
#>

function Find-DeclaredCommand
{
    Param(
        [Parameter(Mandatory = $True, ValueFromPipeline = $True)]
        [String]
        $Path
    )
    $RunbookPaths = Get-ChildItem -Path $Path -Include '*.ps1' -Recurse

    $DeclaredCommandMap = @{}
    foreach ($Path in $RunbookPaths) 
    {
        $Tokens = [System.Management.Automation.PSParser]::Tokenize((Get-Content -Path $Path), [ref] $Null)
        For($i = 0 ; $i -lt $Tokens.Count - 1 ; $i++)
        {
            $Token = $Tokens[$i]
            if($Token.Type -eq 'Keyword' -and $Token.Content -in @('function', 'workflow'))
            {
                Write-Debug -Message "Found command $($NextToken.Content) in $Path of type $($Token.Content)"
                $NextToken = $Tokens[$i+1]
                $DeclaredCommandMap."$($NextToken.Content)" = @{
                    'Path' = $Path
                    'Type' = $Token.Content
                }
            }
        }
    }
    return $DeclaredCommandMap
}

<#
.SYNOPSIS
    A wrapper around [String]::IsNullOrWhiteSpace.
 
.DESCRIPTION
    Provides a PowerShell function wrapper around [String]::IsNullOrWhiteSpace,
    since PowerShell Workflow will not allow a direct method call.
 
.PARAMETER String
    The string to pass to [String]::IsNullOrWhiteSpace.
#>

Function Test-IsNullOrWhiteSpace
{
    [OutputType([bool])]
    Param([Parameter(Mandatory = $True, ValueFromPipeline = $True)]
    [AllowNull()]
    $String)
    Return [String]::IsNullOrWhiteSpace($String)
}

<#
.SYNOPSIS
    A wrapper around [String]::IsNullOrEmpty.
 
.DESCRIPTION
    Provides a PowerShell function wrapper around [String]::IsNullOrEmpty,
    since PowerShell Workflow will not allow a direct method call.
 
.PARAMETER String
    The string to pass to [String]::IsNullOrEmpty.
#>

Function Test-IsNullOrEmpty
{
    [OutputType([bool])]
    Param(
        [Parameter(Mandatory = $True, ValueFromPipeline = $True)]
        [AllowNull()]
        $String
    )
    Return [String]::IsNullOrEmpty($String)
}
<#
.Synopsis
    Takes a pscustomobject and converts into a IDictionary.
    Translates all membertypes into keys for the IDictionary
     
.Parameter InputObject
    The input pscustomobject object to convert
 
.Parameter MemberType
    The membertype to change into a key property
 
.Parameter KeyFilterScript
    A script to run to manipulate the keyname during grouping.
#>

Function ConvertFrom-PSCustomObject
{
    [OutputType([hashtable])] 
    Param(
        [Parameter(Mandatory = $True, ValueFromPipeline = $True)] 
        [AllowNull()]
        $InputObject,

        [Parameter(Mandatory = $False)]
        [System.Management.Automation.PSMemberTypes]
        $MemberType = [System.Management.Automation.PSMemberTypes]::NoteProperty,

        [Parameter(Mandatory = $False)]
        [ScriptBlock] 
        $KeyFilterScript = {
            Param($KeyName) $KeyName 
        }
    ) 
    
    $outputObj = @{}   
    
    foreach($KeyName in ($InputObject | Get-Member -MemberType $MemberType).Name) 
    {
        $KeyName = Invoke-Command -ScriptBlock $KeyFilterScript -ArgumentList $KeyName
        if(-not (Test-IsNullOrEmpty -String $KeyName))
        {
            if($outputObj.ContainsKey($KeyName))
            {
                $outputObj += $InputObject."$KeyName"
            }
            else
            {
                $Null = $outputObj.Add($KeyName, $InputObject."$KeyName")
            } 
        }
    } 
    return $outputObj 
} 

<#
.Synopsis
    Converts an object or array of objects into a hashtable
    by grouping them by the target key property
     
.Parameter InputObject
    The object or array of objects to convert
 
.Parameter KeyName
    The name of the property to group the objects by
 
.Parameter KeyFilterScript
    A script to run to manipulate the keyname during grouping.
#>

Function ConvertTo-Hashtable
{
    [OutputType([hashtable])]
    Param(
        [Parameter(Mandatory = $True, ValueFromPipeline = $True)]
        [AllowNull()]
        $InputObject,

        [Parameter(Mandatory = $True)][string]
        
        $KeyName,
        [Parameter(Mandatory = $False)][ScriptBlock]
        $KeyFilterScript = {
            Param($Key) $Key 
        }
    )
    $outputObj = @{}
    foreach($Object in $InputObject)
    {
        $Key = $Object."$KeyName"
        $Key = Invoke-Command -ScriptBlock $KeyFilterScript -ArgumentList $Key
        if(-not (Test-IsNullOrEmpty -String $Key))
        {
            if($outputObj.ContainsKey($Key))
            {
                $outputObj[$Key] += $Object
            }
            else
            {
                $Null = $outputObj.Add($Key, @($Object))
            }
        }
    }
    return $outputObj
}
<#
    .Synopsis
    Updates the local powershell environment path. Sets the target path as a part
    of the environment path if it does not already exist there
     
    .Parameter Path
    The path to add to the system environment variable 'path'. Only adds if it is not already there
#>

Function Add-PSEnvironmentPathLocation
{
    Param(
        [Parameter(
            Mandatory = $True,
            ValueFromPipeline = $True,
            Position = 0

        )]
        $Path,

        [Parameter(
            Mandatory = $False,
            ValueFromPipeline = $True,
            Position = 1
        )]
        [System.EnvironmentVariableTarget]
        $Location = [System.EnvironmentVariableTarget]::User
    )
    
    $CurrentPSModulePath = [System.Environment]::GetEnvironmentVariable('PSModulePath', $Location) -as [string]
    if(-not($CurrentPSModulePath.ToLower().Contains($Path.ToLower())))
    {
        Write-Verbose -Message "The path [$Path] was not in the environment path [$CurrentPSModulePath]. Adding."
        if($CurrentPSModulePath -as [bool])
        {
            [Environment]::SetEnvironmentVariable( 'PSModulePath', "$CurrentPSModulePath;$Path", $Location )
        }
        else
        {
            [Environment]::SetEnvironmentVariable( 'PSModulePath', "$Path", $Location )
        }

    }
}
<#
    .Synopsis
        Looks for the tag workflow in a file and returns the next string
     
    .Parameter FilePath
        The path to the file to search
#>

Function Get-WorkflowNameFromFile
{
    Param([Parameter(Mandatory=$true)][string] $FilePath)

    $DeclaredCommands = Find-DeclaredCommand -Path $FilePath
    Foreach($Command in $DeclaredCommands.Keys)
    {
        if($DeclaredCommands.$Command.Type -eq 'Workflow') 
        { 
            return $Command -as [string]
        }
    }

    Throw-Exception -Type 'NoWorkflowDefined' -Message 'No workflow defined in file'
}

<#
    .Synopsis
        Tests to see if a file has a PS workflow defined inside of it
     
    .Parameter FilePath
        The path to the file to search
#>

Function Test-FileIsWorkflow
{
    Param([Parameter(Mandatory=$true)][string] $FilePath)

    try { Get-WorkflowNameFromFile -FilePath $FilePath }
    catch
    {
        $Exception = $_
        $ExceptionInformation = Get-ExceptionInfo -Exception $Exception
        Switch ($ExceptionInformation.FullyQualifiedErrorId)
        {
            'NoWorkflowDefined' { return $false }
        }
    }
    return $true
}

<#
    .Synopsis
        Gets a script name based on the filename
     
    .Parameter FilePath
        The path to the file
#>

Function Get-ScriptNameFromFileName
{
    Param([Parameter(Mandatory=$true)][string] $FilePath)
    $CompletedParams = Write-StartingMessage -Stream Debug

    $MatchRegex = '([^\.]+)' -as [string]
    $FileInfo = Get-Item -Path $FilePath
    if($FileInfo.Name -match $MatchRegex)
    {
        Return $Matches[1]
    }
    else
    {
        Throw-Exception -Type 'CouldNotDetermineName' -Message 'Could not determine the script name'
    }

    Write-CompletedMessage @CompletedParams
}

<#
    .Synopsis
        Gets a script name based on the filename
     
    .Parameter FilePath
        The path to the file
#>

Function Get-DSCConfigurationName
{
    Param([Parameter(Mandatory=$true)][string] $FilePath)
    $CompletedParams = Write-StartingMessage -Stream Debug

    $MatchRegex = 'Configuration[\s]+([^{\s]+)[\s]*' -as [string]
    $FileContent = (Get-Content $FilePath) -as [string]
    if($FileContent -Match $MatchRegex)
    {
        $Name = $Matches[1]
    }
    else
    {
        Throw-Exception -Type 'CouldNotDetermineName' -Message 'Could not determine the configuration name'
    }

    Write-CompletedMessage @CompletedParams -Status $Name
    Return $Name
}

<#
    .Synopsis
        Gets a script name based on the filename
     
    .Parameter FilePath
        The path to the file
#>

Function Get-DSCNodeName
{
    Param([Parameter(Mandatory=$true)][string] $FilePath)
    $CompletedParams = Write-StartingMessage -Stream Debug

    $MatchRegex = 'Node[\s]+([^{\s]+)[\s]*' -as [regex]
    $FileContent = (Get-Content $FilePath) -as [string]
    $Match = $MatchRegex.Matches($FileContent)
    $ReturnObj = $Match | ForEach-Object { ($_.Groups[1].Value.Replace('''','')).Replace('"','') }
    
    Write-CompletedMessage @CompletedParams -Status ($ReturnObj | ConvertTo-Json)
    Return $ReturnObj
}

<#
.SYNOPSIS
    Given a hashtable, filters entries based on their value. Returns
    a new hashtable whose elements are only those whose value cause
    $FilterScript to return $True.
 
.PARAMETER Hashtable
    The hashtable to filter.
 
.PARAMETER FilterScript
    The filter script to apply to each element of the hashtable.
#>

Function Select-Hashtable
{
    param(
        [Parameter(Mandatory=$True)]  [Hashtable] $Hashtable,
        [Parameter(Mandatory=$False)] [ScriptBlock] $FilterScript = { $_ -as [Bool] }
    )

    $FilteredHashtable = @{}
    foreach($Element in $Hashtable.GetEnumerator())
    {
        $_ = $Element.Value
        if($FilterScript.InvokeWithContext($null, (Get-Variable -Name '_'), $null))
        {
            $FilteredHashtable[$Element.Name] = $Element.Value
        }
    }
    return $FilteredHashtable
}
<#
.Synopsis
    Writes a finished verbose message
#>

function Write-CompletedMessage
{
    Param(
        [Parameter(Mandatory=$True)]
        [datetime]
        $StartTime,

        [Parameter(Mandatory=$True)]
        [String]
        $Name,

        [Parameter(Mandatory=$False)]
        [String]
        $Status = $Null,

        [Parameter(Mandatory=$False)]
        [ValidateSet('Debug', 'Error', 'Verbose', 'Warning', 'Automation')]
        [String]
        $Stream = 'Automation',

        [Parameter(Mandatory=$False)]
        [switch]
        $PassThru
    )
    
    if($Name -notmatch '^\[.*]$')
    {
        $Name = "[$Name]"
    }
    if($Status -as [bool])
    {
        $Name = "$Name [$Status]"
    } 
    $LogCommand = (Get-Command -Name "Write-$Stream")
    $EndTime = Get-Date
    $ElapsedTime = ($EndTime - $StartTime).TotalSeconds
    $Message = "Completed $Name in [$($ElapsedTime)] Seconds"
    & $LogCommand -Message $Message `
                  -WarningAction Continue

    if($PassThru.IsPresent)
    {
        Write-Output -InputObject @{
            'Name' = $Name
            'StartTime' = $StartTime
            'EndTime' = $EndTime
            'ElapsedTime' = $ElapsedTime
            'Message' = $Message
        }
    }
}

<#
.Synopsis
    Writes the standard starting message
.Parameter String
    An additional string to write out
.Parameter Stream
    The stream to write the starting message to
#>

function Write-StartingMessage
{
    Param(
        [Parameter(Mandatory=$False)]
        [String]
        $CommandName = '',

        [Parameter(Mandatory=$False)]
        [String]
        $String = '',

        [Parameter(Mandatory=$False)]
        [ValidateSet('Debug', 'Error', 'Verbose', 'Warning', 'Automation')]
        [String]
        $Stream = 'Automation'
    )
    
    $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop

    $_CommandName = Select-FirstValid $Commandname, ((Get-PSCallStack)[1].Command -as [string])
    $Name = [string]::Empty
    if($_CommandName -as [bool])
    {
        if($String -as [bool])
        {
            $Name = "[$_CommandName] [$String]"
        }
        else
        {
            $Name = "[$_CommandName]"
        }
    }
    elseif($String -as [bool])
    {
        $Name = "[$String]"
    }
    else
    {
        $ExceptionMessage = 'Could not determine the current name.
                             Please pass the -string parameter with
                             $WorkflowCommandName if running from workflow'
 -replace "`r`n", ' ' -replace ' ', ''
        Throw-Exception -Type 'CannotDetermineName' `
                        -Message $ExceptionMessage
    }

    $LogCommand = (Get-Command -Name "Write-$Stream")
    & $LogCommand -Message "Starting $Name" `
                  -WarningAction Continue `
                  -ErrorAction Continue
    
    Return @{ 'Name' = $Name ; 'StartTime' = (Get-Date) ; 'Stream' = $Stream}
}
Function Start-SleepUntil
{
    Param(
        [Parameter(Mandatory=$False)]
        [DateTime]
        $DateTime = (Get-Date).AddSeconds(1)
    )
    $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop
    $CompletedParams = Write-StartingMessage -String "DateTime [$DateTime]"
    $SleepSeconds = ($DateTime - (Get-Date)).TotalSeconds
    if($SleepSeconds -gt 0)
    {
        Write-Verbose -Message "Sleeping for [$SleepSeconds] seconds"
        Start-Sleep -Seconds $SleepSeconds
    }
    Write-CompletedMessage @CompletedParams
}

<#
.Synopsis
    Generates one or more complex strings designed to fulfill the requirements for Active Directory
 
.DESCRIPTION
    Generates one or more complex strings designed to fulfill the requirements for Active Directory
 
.EXAMPLE
    New-RandomString
    C&3SX6Kn
 
    Will generate one password with a length between 8 and 12 chars.
.EXAMPLE
    New-RandomString -MinLength 8 -MaxLength 12 -Count 4
    7d&5cnaB
    !Bh776T"Fw
    9"C"RxKcY
    %mtM7#9LQ9h
 
    Will generate four passwords, each with a length of between 8 and 12 chars.
.EXAMPLE
    New-RandomString -InputString abc, ABC, 123 -Length 4
    3ABa
 
    Generates a password with a length of 4 containing atleast one char from each InputString
.EXAMPLE
    New-RandomString -InputString abc, ABC, 123 -Length 4 -FirstChar abcdefghijkmnpqrstuvwxyzABCEFGHJKLMNPQRSTUVWXYZ
    3ABa
 
    Generates a password with a length of 4 containing atleast one char from each InputString that will start with a letter from
    the string specified with the parameter FirstChar
.OUTPUTS
    [String]
.FUNCTIONALITY
    Generates random strings
#>

Function New-RandomString
{
    [CmdletBinding(DefaultParameterSetName='FixedLength',ConfirmImpact='None')]
    [OutputType([String])]
    Param
    (
        # Specifies minimum length
        [Parameter(
            Mandatory=$false,
            ParameterSetName='RandomLength'
        )]
        [ValidateScript({$_ -gt 0})]
        [Alias('Min')] 
        [int]
        $MinLength = 8,
        
        # Specifies maximum length
        [Parameter(
            Mandatory=$false,
            ParameterSetName='RandomLength'
        )]
        [ValidateScript({
            if($_ -ge $MinPasswordLength) { $true }
            else { Throw 'Max value cannot be lesser than min value.' }
        })]
        [Alias('Max')]
        [int]
        $MaxLength = 12,

        # Specifies a fixed length
        [Parameter(
            Mandatory=$false,
            ParameterSetName='FixedLength'
        )]
        [ValidateRange(1,2147483647)]
        [int]
        $Length = 8,
        
        # Specifies an array of strings containing charactergroups from which the password will be generated.
        # At least one char from each group (string) will be used.
        [String[]]
        $InputString = @(
            'abcdefghijkmnpqrstuvwxyz',
            'ABCEFGHJKLMNPQRSTUVWXYZ',
            '23456789',
            '!"#%&'
        ),

        # Specifies a string containing a character group from which the first character in the password will be generated.
        # Useful for systems which requires first char in password to be alphabetic.
        [String]
        $FirstChar,
        
        # Specifies number of passwords to generate.
        [ValidateRange(1,2147483647)]
        [int]
        $Count = 1
    )
    Begin 
    {
        Function Get-Seed
        {
            # Generate a seed for randomization
            $RandomBytes = New-Object -TypeName 'System.Byte[]' 4
            $Random = New-Object -TypeName 'System.Security.Cryptography.RNGCryptoServiceProvider'
            $Random.GetBytes($RandomBytes)
            [BitConverter]::ToUInt32($RandomBytes, 0)
        }
    }
    Process
    {
        $CompletedParams = Write-StartingMessage
        For($iteration = 1;$iteration -le $Count; $iteration++)
        {
            $Password = @{}
            # Create char arrays containing groups of possible chars
            [char[][]]$CharGroups = $InputString

            # Create char array containing all chars
            $AllChars = $CharGroups | ForEach-Object {[Char[]]$_}

            # Set password length
            if($PSCmdlet.ParameterSetName -eq 'RandomLength')
            {
                if($MinLength -eq $MaxLength) {
                    # If password length is set, use set length
                    $Length = $MinLength
                }
                else {
                    # Otherwise randomize password length
                    $Length = ((Get-Seed) % ($MaxLength + 1 - $MinLength)) + $MinLength
                }
            }

            # If FirstChar is defined, randomize first char in password from that string.
            if($PSBoundParameters.ContainsKey('FirstChar')){
                $Password.Add(0,$FirstChar[((Get-Seed) % $FirstChar.Length)])
            }
            # Randomize one char from each group
            Foreach($Group in $CharGroups) {
                if($Password.Count -lt $Length) {
                    $Index = Get-Seed
                    While ($Password.ContainsKey($Index)){
                        $Index = Get-Seed                        
                    }
                    $Password.Add($Index,$Group[((Get-Seed) % $Group.Count)])
                }
            }

            # Fill out with chars from $AllChars
            for($i=$Password.Count;$i -lt $Length;$i++) {
                $Index = Get-Seed
                While ($Password.ContainsKey($Index)){
                    $Index = Get-Seed                        
                }
                $Password.Add($Index,$AllChars[((Get-Seed) % $AllChars.Count)])
            }
            $RetObj = $(-join ($Password.GetEnumerator() | Sort-Object -Property Name | Select-Object -ExpandProperty Value))
            Write-Output -InputObject $RetObj
        }
        Write-CompletedMessage @CompletedParams -Status $RetObj
    }
}

function Write-Automation
{
    Param(
        [Parameter(Mandatory=$False)]
        [String]
        $Message = ''
    )
    
    $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop

    $Source = 'JobLog'
    $LogName = 'AzureAutomationLog'

    If ($Script:WriteVerbose -eq $null)
    {
        $Script:WriteVerbose = $False
    }
        
    Try
    {
        #Get the Hybrid Worker configuration (only if it's not previously retrieved)
        If ($Script:HybridWorkerConfig -eq $null)
        {
            $Script:HybridWorkerConfig = GetHybridWorkerConfiguration
        }
    }
    Catch
    {
        $Script:WriteVerbose = $True
        $Exception = $_
        $ExceptionInfo = Get-ExceptionInfo -Exception $Exception
        Switch($ExceptionInfo.Type)
        {
            'NotAHybridRunbookWorker'
            {
                $Script:HybridWorkerConfig = @{
                    HybridWorkerGroupName = [string]::Empty
                    AutomationAccountId = [string]::Empty
                    MachineId = [string]::Empty
                    ComputerName = [string]::Empty
                    MMAInstallRoot = [string]::Empty
                    PSVersion = [string]::Empty
                    HybridWorkerVersion = [string]::Empty
                    SystemProxy = [string]::Empty
                    MMAVersion = [string]::Empty
                    MMAProxyUrl = [string]::Empty
                    MMAProxyUserName = [string]::Empty
                    MMAOMSWorkspaceId = [string]::Empty
                }
            }
            Default
            {
                Throw
            }
        }
    }

    Try
    {
        #Get the runbook runtime info (only if it's not previously retrieved)
        If ($Script:RunbookRuntimeInfo -eq $null)
        {
            $Script:RunbookRuntimeInfo = GetHybridWorkerJobRuntimeInfo
        }
    }
    Catch
    {
        $Script:WriteVerbose = $True
        $Exception = $_
        $ExceptionInfo = Get-ExceptionInfo -Exception $Exception
        Switch($ExceptionInfo.Type)
        {
            'NotExecutedAtHybridRunbookWorkerRuntime'
            {
                $Script:RunbookRuntimeInfo = @{
                    JobId = [string]::Empty
                    SandboxId = [string]::Empty
                    ProcessId = [string]::Empty
                    AutomationAssetEndPoint = [string]::Empty
                    PSModulePath = [string]::Empty
                    CurrentUser = [string]::Empty
                    LogActivityTrace = [string]::Empty
                    CurrentWorkingDirectory = [string]::Empty
                    RunbookType = [string]::Empty
                    RunbookName = [string]::Empty
                    AccountName = [string]::Empty
                    ResourceGroupName = [string]::Empty
                    SubscriptionId = [string]::Empty
                    TimeTakenToStartRunninginSeconds = [string]::Empty
                }
            }
            Default
            {
                Throw
            }
        }
    }

    #make sure the event source exists
    if ([System.Diagnostics.EventLog]::SourceExists($source) -eq $false)
    {
        [System.Diagnostics.EventLog]::CreateEventSource($source, $LogName)
    }

    Write-EventLog -LogName $LogName -Source $Source -EntryType Information -EventId 1 -Message (@{
        'Message' = $Message
        'RunbookRuntimeInfo' = $Script:RunbookRuntimeInfo
        'HybridWorkerConfig' = $Script:HybridWorkerConfig
    } | ConvertTo-Json)

    if($WriteVerbose) { Write-Verbose -Message $Message }
}

Function GetHybridWorkerConfiguration
{
    Param(
    )
    
    $HybridRunbookWorkerKeyPath = 'HKLM:\SOFTWARE\Microsoft\HybridRunbookWorker'
    
    #Read Hybrid Worker reg key
    if(-not (Test-Path -Path $HybridRunbookWorkerKeyPath))
    {
        Throw-Exception -Type 'NotAHybridRunbookWorker' -Message 'Current Machine is not a Hybrid Runbook Worker'
    }

    $HybridWorkerRegKeyValues = Get-ItemProperty -Path $HybridRunbookWorkerKeyPath

    #Get Hybrid worker version
    $SandboxAssembly = [AppDomain]::CurrentDomain.GetAssemblies() | Where-object {$_.ManifestModule.Name -eq 'Orchestrator.Sandbox.exe'}
    If ($SandboxAssembly)
    {
        $HybridWorkerVersion = split-path (split-path (split-path $SandboxAssembly.location)) -leaf
    }

    #Get System Proxy
    $SystemProxyBytes = (Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings\Connections" -Name WinHttpSettings).WinHttPSettings
    $SystemProxyLength = $SystemProxyBytes[12]
    if ($SystemProxyLength -gt 0)
    {
        $SystemProxy = -join ($SystemProxyBytes[(12+3+1)..(12+3+1+$SystemProxyLength-1)] | ForEach-Object {([char]$_)})
    } else {
        $SystemProxy = $null
    }

    #Get MMA configuration
    $MMAConfig = New-Object -ComObject 'AgentConfigManager.MgmtSvcCfg'
    $MMAVersion = (Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Microsoft Operations Manager\3.0\Setup").AgentVersion
    $HybridWorkerConfig = @{
        HybridWorkerGroupName = $HybridWorkerRegKeyValues.RunbookWorkerGroup
        AutomationAccountId = $HybridWorkerRegKeyValues.AccountId
        MachineId = $HybridWorkerRegKeyValues.MachineId
        ComputerName = $env:COMPUTERNAME
        MMAInstallRoot = $env:MOMROOT
        PSVersion = $host.Version.ToString()
        HybridWorkerVersion = $HybridWorkerVersion
        SystemProxy = $SystemProxy
        MMAVersion = $MMAVersion
        MMAProxyUrl = $MMAConfig.ProxyUrl
        MMAProxyUserName = $MMAConfig.ProxyUsername
        MMAOMSWorkspaceId = $MMAConfig.AzureOperationalInsightsWorkspaceId
    }
    Return $HybridWorkerConfig
}

Function GetHybridWorkerJobRuntimeInfo
{
    #Make sure this function is executed within a runbook
    If ($PSPrivateMetadata -eq $null -and $env:AUTOMATION_ASSET_SANDBOX_ID -eq $null)
    {
        Throw-Exception -Type 'NotExecutedAtHybridRunbookWorkerRuntime' `
                        -Message 'GetHybridWorkerJobRuntimeInfo function must be executed within an Azure Automation runbook executed on a Hybrid Worker.'
    }

    #Get job details from Windows event log
    $SandboxId = $env:AUTOMATION_ASSET_SANDBOX_ID
    $5532EventFilter = @"
<QueryList>
    <Query Id='0' Path='Microsoft-SMA/Operational'>
        <Select Path='Microsoft-SMA/Operational'>*[System[(EventID=5532)]] and *[System[(Level=4)]] and *[EventData[Data[@Name='sandboxId']='{$SandboxId}']]</Select>
    </Query>
</QueryList>
"@

    $3732EventFilter = @"
<QueryList>
    <Query Id='0' Path='Microsoft-SMA/Operational'>
        <Select Path='Microsoft-SMA/Operational'>*[System[(EventID=3732)]] and *[System[(Level=4)]] and *[EventData[Data[@Name='sandboxId']='{$SandboxId}']]</Select>
    </Query>
</QueryList>
"@

    #$LogEntry = Get-WinEvent -FilterHashtable $FilterHastable
    $3732LogEntry = Get-WinEvent -FilterXml $3732EventFilter
    $5532LogEntry = Get-WinEvent -FilterXml $5532EventFilter
    $3732LogEntryXML = [XML]$3732LogEntry.ToXml()
    $5532LogEntryXML = [XML]$5532LogEntry.ToXml()
    
    #Convert to hashtable
    $JobEventDetails = @{}
    Foreach ($item in $5532LogEntryXML.Event.EventData.Data)
    {
        $JobEventDetails.Add($Item.Name, $Item.'#text'.Trim("{ }"))
    }

    #Get account name, resource group name and subscription Id
    $RunbookType = ($3732LogEntryXML.Event.EventData.Data | Where-Object {$_.name -ieq 'runbookType'}).'#text'.Trim("{ }")
    $JobEventDetails.Add('RunbookType', $RunbookType)

    $JobInfo = @{
        JobId = $JobEventDetails.JobId;
        SandboxId = $SandboxId
        ProcessId = $PID
        AutomationAssetEndPoint = $env:AUTOMATION_ASSET_ENDPOINT
        PSModulePath = $Env:PSModulePath
        CurrentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
        LogActivityTrace = $PSPrivateMetadata.LogActivityTrace
        CurrentWorkingDirectory = $PWD.ToString()
        RunbookType = $JobEventDetails.runbookType
        RunbookName = $JobEventDetails.runbookName
        AccountName = $JobEventDetails.accountName
        ResourceGroupName = $JobEventDetails.resourceGroupName
        SubscriptionId = $JobEventDetails.subscriptionId
        TimeTakenToStartRunninginSeconds = $JobEventDetails.timeTakenToStartRunningInSeconds
    }
    return $JobInfo
}
Export-ModuleMember -Function *-* -Verbose:$False -Debug:$False