JEAnalyzer.psm1

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

# Detect whether at some level dotsourcing was enforced
$script:doDotSource = Get-PSFConfigValue -FullName JEAnalyzer.Import.DoDotSource -Fallback $false
if ($JEAnalyzer_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 JEAnalyzer.Import.IndividualFiles -Fallback $false
if ($JEAnalyzer_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
    )
    
    if ($doDotSource) { . (Resolve-Path $Path).ProviderPath }
    else { $ExecutionContext.InvokeCommand.InvokeScript($false, ([scriptblock]::Create([io.file]::ReadAllText((Resolve-Path $Path).ProviderPath))), $null, $null) }
}

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

#region Load compiled code
# Load English strings
Import-PSFLocalizedString -Path "$script:ModuleRoot\en-us\strings.psd1" -Module JEAnalyzer -Language en-US

# Obtain strings variable for in-script use
$script:strings = Get-PSFLocalizedString -Module JEAnalyzer

function Get-CommandMetaData
{
<#
    .SYNOPSIS
        Processes extra meta-information for a command
     
    .DESCRIPTION
        Processes extra meta-information for a command
     
    .PARAMETER CommandName
        The command to add information for.
     
    .PARAMETER File
        The file the command was read from.
     
    .EXAMPLE
        PS C:\> Get-CommandMetaData -CommandName 'Get-Help'
     
        Adds additional information for Get-Help and returns a useful data object.
#>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [string[]]
        $CommandName,
        
        [string]
        $File
    )
    
    begin
    {
        Write-PSFMessage -Level InternalComment -String 'General.BoundParameters' -StringValues ($PSBoundParameters.Keys -join ", ") -Tag 'debug', 'start', 'param'
        
        if (-not $script:allcommands)
        {
            # Cache known commands once
            Write-PSFMessage -Level Warning -Message "Gathering command information for the first time. This may take quite a while."
            [System.Collections.ArrayList]$script:allcommands = Get-Command | Group-Object Name | ForEach-Object { $_.Group | Sort-Object Version -Descending | Select-Object -First 1 }
            Get-Alias | Where-Object Name -NotIn $script:allcommands.Name | ForEach-Object { $null = $script:allcommands.Add($_) }
        }
    }
    process
    {
        foreach ($command in $CommandName)
        {
            Write-PSFMessage -Level Verbose -Message "Adding meta information for: $($command)"
            $commandObject = New-Object -TypeName 'JEAnalyzer.CommandInfo' -Property @{
                CommandName   = $command
                File          = $File
            }
            if ($object = $script:allcommands | Where-Object Name -EQ $command) { $commandObject.CommandObject = $object }
            $commandObject | Select-PSFObject -KeepInputObject -ScriptProperty @{
                IsDangerous = {
                    # Parameters that accept scriptblocks are assumed to be dangerous
                    if ($this.CommandObject.Parameters.Values | Where-Object { $_.ParameterType.FullName -eq 'System.Management.Automation.ScriptBlock' }) { return $true }
                    
                    # If the command is flagged as dangerous for JEA, mark it as such
                    if ($this.CommandObject.Definition -match 'PSFramework\.PSFCore\.NoJeaCommand') { return $true }
                    
                    # If the command has a parameter flagged as dangerous for JEA, the command is a danger
                    if ($this.CommandObject.Parameters.Values | Where-Object { $_.Attributes | Where-Object { $_ -is [PSFramework.PSFCore.NoJeaParameterAttribute] } }) { return $true }
                        
                    # Default: Is the command blacklisted?
                    (& (Get-Module JEAnalyzer) { $script:dangerousCommands }) -contains $this.CommandName
                }
            }
        }
    }
}

function Read-Script {
<#
    .SYNOPSIS
        Parse the content of a script
     
    .DESCRIPTION
        Uses the powershell parser to parse the content of a script or scriptfile.
     
    .PARAMETER ScriptCode
        The scriptblock to parse.
     
    .PARAMETER Path
        Path to the scriptfile to parse.
        Silently ignores folder objects.
     
    .EXAMPLE
        PS C:\> Read-PSMDScript -ScriptCode $ScriptCode
         
        Parses the code in $ScriptCode
     
    .EXAMPLE
        PS C:\> Get-ChildItem | Read-PSMDScript
         
        Parses all script files in the current directory
     
    .NOTES
        Additional information about the function.
#>

    [CmdletBinding()]
    param (
        [Parameter(Position = 0, ParameterSetName = 'Script', Mandatory = $true)]
        [System.Management.Automation.ScriptBlock]$ScriptCode,
        
        [Parameter(Mandatory = $true, ParameterSetName = 'File', ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('FullName')]
        [string[]]$Path
    )
    
    process {
        foreach ($file in $Path) {
            Write-PSFMessage -Level Verbose -Message "Processing $file" -Target $file
            $item = Get-Item $file
            if ($item.PSIsContainer) {
                Write-PSFMessage -Level Verbose -Message "is folder, skipping $file" -Target $file
                continue
            }
            
            $tokens = $null
            $errors = $null
            $ast = [System.Management.Automation.Language.Parser]::ParseFile($item.FullName, [ref]$tokens, [ref]$errors)
            [pscustomobject]@{
                PSTypeName = 'PSModuleDevelopment.Meta.ParseResult'
                Ast           = $ast
                Tokens       = $tokens
                Errors       = $errors
                File       = $item.FullName
            }
        }
        
        if ($ScriptCode) {
            $tokens = $null
            $errors = $null
            $ast = [System.Management.Automation.Language.Parser]::ParseInput($ScriptCode, [ref]$tokens, [ref]$errors)
            [pscustomobject]@{
                PSTypeName = 'PSModuleDevelopment.Meta.ParseResult'
                Ast           = $ast
                Tokens       = $tokens
                Errors       = $errors
                Source       = $ScriptCode
            }
        }
    }
}

function Add-JeaModuleRole
{
<#
    .SYNOPSIS
        Adds JEA roles to JEA Modules.
     
    .DESCRIPTION
        Adds JEA roles to JEA Modules.
     
    .PARAMETER Module
        The module to add roles to.
        Create a new module by using New-JeaModule command.
     
    .PARAMETER Role
        The role(s) to add.
        Create a new role by using the New-JeaRole command.
     
    .PARAMETER Force
        Enforce adding the role, overwriting existing roles of the same name.
     
    .PARAMETER EnableException
        This parameters disables user-friendly warnings and enables the throwing of exceptions.
        This is less user friendly, but allows catching exceptions in calling scripts.
     
    .EXAMPLE
        PS C:\> $roles | Add-JeaModuleRole -Module $module
     
        Adds the roles stored in $roles to the module stored in $module
#>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, Position = 0)]
        [JEAnalyzer.Module]
        $Module,
        
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [JEAnalyzer.Role[]]
        $Role,
        
        [switch]
        $Force,
        
        [switch]
        $EnableException
    )
    
    process
    {
        foreach ($roleItem in $Role)
        {
            if ($Module.Roles.ContainsKey($roleItem.Name) -and -not $Force)
            {
                Stop-PSFFunction -String 'Add-JeaModuleRole.RolePresent' -StringValues $roleItem.Name, $Module.Name -EnableException $EnableException -Continue -Cmdlet $PSCmdlet -Target $Role
            }
            Write-PSFMessage -String 'Add-JeaModuleRole.AddingRole' -StringValues $roleItem.Name, $Module.Name -Target $Role
            $Module.Roles[$roleItem.Name] = $roleItem
        }
    }
}

function Add-JeaModuleScript
{
<#
    .SYNOPSIS
        Adds a script to a JEA module.
     
    .DESCRIPTION
        Adds a script to a JEA module.
        This script will be executed on import, either before or after loading functiosn contained in the module.
        Use this to add custom logic - such as logging - as users connect to the JEA endpoint.
     
    .PARAMETER Module
        The JEA module to add the script to.
        Use New-JeaModule to create such a module object.
     
    .PARAMETER Path
        Path to the scriptfile to add.
     
    .PARAMETER Text
        Script-Code to add.
     
    .PARAMETER Name
        Name of the scriptfile.
        This parameter is optional. What happens if you do NOT use it depends on other parameters:
        -Path : Uses the filename instead
        -Text : Uses a random guid
        This is mostly cosmetic, as you would generally not need to manually modify the output module.
     
    .PARAMETER Type
        Whether the script is executed before or after the functions of the JEA module are available.
        It needs to run BEFORE loading the functions if defining PowerShell classes, AFTER if it uses the functions.
        If neither: Doesn't matter.
        Defaults to: PostScript
     
    .EXAMPLE
        PS C:\> Add-JeaModuleScript -Module $Module -Path '.\connect.ps1'
     
        Adds the connect.ps1 scriptfile as a script executed after loading functions.
#>

    [CmdletBinding(DefaultParameterSetName = 'File')]
    Param (
        [Parameter(Mandatory = $true, Position = 0)]
        [JEAnalyzer.Module]
        $Module,
        
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'File')]
        [PsfValidateScript('PSFramework.Validate.FSPath.File', ErrorString = 'PSFramework.Validate.FSPath.File')]
        [Alias('FullName')]
        [string]
        $Path,
        
        [Parameter(Mandatory = $true, ParameterSetName = 'Text')]
        [string]
        $Text,
        
        [string]
        $Name,
        
        [ValidateSet('PreScript','PostScript')]
        [string]
        $Type = 'PostScript'
    )
    
    process
    {
        if ($Path)
        {
            $file = [JEAnalyzer.ScriptFile]::new($Path)
            if ($Name) { $file.Name = $Name }
        }
        else
        {
            if (-not $Name) { $Name = [System.Guid]::NewGuid().ToString() }
            $file = [JEAnalyzer.ScriptFile]::new($Name, $Text)
        }
        switch ($Type)
        {
            'PreScript' { $Module.PreimportScripts[$file.Name] = $file }
            'PostScript' { $Module.PostimportScripts[$file.Name] = $file }
        }
    }
}

function ConvertTo-JeaCapability
{
<#
    .SYNOPSIS
        Converts the input into JEA Capabilities.
     
    .DESCRIPTION
        Converts the input into JEA Capabilities.
        This is a multitool conversion command, accepting a wide range of input objects.
        Whether it is a simple command name, the output of Get-Command, items returned by Read-JeaScriptFile or a complex hashtable.
        Example hashtable:
        @{
            'Get-Service' = @{
                Name = 'Restart-Service'
                Parameters = @{
                    Name = 'Name'
                    ValidateSet = 'dns', 'spooler'
                }
            }
        }
     
    .PARAMETER InputObject
        The object(s) to convert into a capability object.
     
    .EXAMPLE
        PS C:\> Get-Command Get-AD* | ConvertTo-JeaCapability
     
        Retrieves all ad commands that read data and converts them into capabilities.
#>

    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline = $true)]
        $InputObject
    )
    
    process
    {
        foreach ($inputItem in $InputObject)
        {
            # Skip empty entries
            if ($null -eq $inputItem) { continue }
            
            # Pass through finished capabilities
            if ($inputItem -is [JEAnalyzer.Capability])
            {
                $inputItem
                continue
            }
            
            #region Decide based on input type
            switch ($inputItem.GetType().FullName)
            {
                #region Get-Command data
                'System.Management.Automation.AliasInfo'
                {
                    New-Object -TypeName JEAnalyzer.CapabilityCommand -Property @{
                        Name = $inputItem.ResolvedCommand.Name
                        CommandType = 'Alias'
                    }
                    break
                }
                'System.Management.Automation.FunctionInfo'
                {
                    New-Object -TypeName JEAnalyzer.CapabilityCommand -Property @{
                        Name = $inputItem.Name
                        CommandType = 'Function'
                    }
                    break
                }
                'System.Management.Automation.CmdletInfo'
                {
                    New-Object -TypeName JEAnalyzer.CapabilityCommand -Property @{
                        Name = $inputItem.Name
                        CommandType = 'Cmdlet'
                    }
                    break
                }
                #endregion Get-Command data
                #region String
                'System.String'
                {
                    if (Test-Path $inputItem) { Import-JeaScriptFile -Path $inputItem }
                    else { Get-Command -Name $inputItem -ErrorAction SilentlyContinue | ConvertTo-JeaCapability }
                    break
                }
                #endregion String
                #region Hashtable
                'System.Collections.Hashtable'
                {
                    #region Plain Single-Item hashtable
                    if ($inputItem.Name)
                    {
                        $parameter = @{
                            Name = $inputItem.Name
                        }
                        if ($inputItem.Parameters) { $parameter['Parameter'] = $inputItem.Parameters }
                        if ($inputItem.Force) { $parameter['Force'] = $true }
                        
                        New-JeaCommand @parameter
                    }
                    #endregion Plain Single-Item hashtable
                    
                    #region Multiple Command Hashtable
                    else
                    {
                        foreach ($valueItem in $inputItem.Values)
                        {
                            $parameter = @{
                                Name = $valueItem.Name
                            }
                            if ($valueItem.Parameters) { $parameter['Parameter'] = $valueItem.Parameters }
                            if ($inputItem.Force -or $valueItem.Force) { $parameter['Force'] = $true }
                            
                            New-JeaCommand @parameter
                        }
                    }
                    #endregion Multiple Command Hashtable
                    
                    break
                }
                #endregion Hashtable
                #region JEAnalyzer: Command Info
                'JEAnalyzer.CommandInfo'
                {
                    $inputItem.CommandObject | ConvertTo-JeaCapability
                    break
                }
                #endregion JEAnalyzer: Command Info
                default
                {
                    Write-PSFMessage -String 'ConvertTo-Capability.CapabilityNotKnown' -StringValues $inputItem -Level Warning
                    break
                }
            }
            #endregion Decide based on input type
        }
    }
}

function Import-JeaScriptFile
{
<#
    .SYNOPSIS
        Loads scriptfiles as JEA Capability.
     
    .DESCRIPTION
        Loads scriptfiles as JEA Capability.
        This will ...
        - convert the specified script into a function,
        - register that function as a capability and
        - add the function as a public function to the module.
     
    .PARAMETER Path
        The path to the file(s).
        Folder items will be skipped.
         
    .PARAMETER FunctionName
        The name to apply to the function.
        Overrides the default function name finding.
     
    .PARAMETER Role
        The role to add the capability to.
        Specifying a role will suppress the object return.
     
    .PARAMETER Encoding
        The encoding in which to read the files.
     
    .PARAMETER EnableException
        This parameters disables user-friendly warnings and enables the throwing of exceptions.
        This is less user friendly, but allows catching exceptions in calling scripts.
     
    .EXAMPLE
        PS C:\> Import-JeaScriptFile -Path '.\script.ps1'
     
        Creates a script capability from the .\script.ps1 file.
        The function added will be named 'Invoke-Script'
     
    .EXAMPLE
        PS C:\> Import-JeaScriptFile -Path .\script.ps1 -FunctionName 'Get-ServerHealth'
     
        Creates a script capability from the .\script.ps1 file.
        The function added will be named 'Get-ServerHealth'
     
    .EXAMPLE
        PS C:\> Get-ChildItem C:\JEA\Role1\*.ps1 | Import-JeaScriptFile -Role $role
     
        Reads all scriptfiles in C:\JEA\Role1, converts them into functions, names them and adds them to the role stored in $role.
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingInvokeExpression', '')]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('FullName')]
        [string[]]
        $Path,
        
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [string]
        $FunctionName,
        
        [JEAnalyzer.Role]
        $Role,
        
        [PSFEncoding]
        $Encoding = (Get-PSFConfigValue -FullName PSFramework.Text.Encoding.DefaultRead),
        
        [switch]
        $EnableException
    )
    
    begin
    {
        #region Utility Functions
        function Test-Function
        {
            [CmdletBinding()]
            param (
                [string]
                $Path
            )
            $tokens = $null
            $errors = $null
            $ast = [System.Management.Automation.Language.Parser]::ParseFile($Path, [ref]$tokens, [ref]$errors)
            if ($errors)
            {
                return [pscustomobject]@{
                    IsFunction = $false
                    ErrorType  = 'ParseError'
                    Errors       = $errors
                }
            }
            elseif ($ast.EndBlock.Statements.Count -ne 1)
            {
                return $false
            }
            elseif ($ast.EndBlock.Statements[0] -is [System.Management.Automation.Language.FunctionDefinitionAst])
            {
                return [pscustomobject]@{
                    IsFunction = $true
                    Name       = $Ast.EndBlock.Statements[0].Name
                }
            }
            return $false
        }
        #endregion Utility Functions
    }
    process
    {
        foreach ($file in (Resolve-PSFPath -Path $Path -Provider FileSystem))
        {
            Write-PSFMessage -String 'Import-JeaScriptFile.ProcessingInput' -StringValues $file
            $fileItem = Get-Item -LiteralPath $Path
            if ($fileItem.PSIsContainer) { continue }
            
            $testResult = Test-Function -Path $file
            #region Case: Script File
            if (-not $testResult)
            {
                $functionName = 'Invoke-{0}' -f $host.CurrentCulture.TextInfo.ToTitleCase(($fileItem.BaseName -replace '\[\]\s','_'))
                if ($fileItem.BaseName -like '*-*')
                {
                    $verb = $fileItem.BaseName -split '-', 2
                    if (Get-Verb -verb $verb) { $functionName = $host.CurrentCulture.TextInfo.ToTitleCase(($fileItem.BaseName -replace '\[\]\s', '_')) }
                }
                if ($Name) { $functionName = $Name }
                
                $functionString = @'
function {0}
{{
{1}
}}
'@
 -f $functionName, ([System.IO.File]::ReadAllText($file, $Encoding))
                
                Invoke-Expression $functionString
                $functionInfo = Get-Command -Name $functionName
                $capability = New-Object -TypeName 'JEAnalyzer.CapabilityScript'
                $capability.Content = $functionInfo
                $capability.Name = $functionInfo.Name
                if ($Role) { $Role.CommandCapability[$capability.Name] = $capability }
                else { $capability }
            }
            #endregion Case: Script File
            
            #region Case: Parse Error
            elseif ($testResult.ErrorType -eq 'ParseError')
            {
                Stop-PSFFunction -String 'Import-JeaScriptFile.ParsingError' -StringValues $file -Continue -EnableException $EnableException
            }
            #endregion Case: Parse Error
            
            #region Case: Function File
            elseif ($testResult.IsFunction)
            {
                . $file
                $functionInfo = Get-Command -Name $testResult.Name
                $capability = New-Object -TypeName 'JEAnalyzer.CapabilityScript'
                $capability.Content = $functionInfo
                $capability.Name = $functionInfo.Name
                if ($Role) { $Role.CommandCapability[$capability.Name] = $capability }
                else { $capability }
            }
            #endregion Case: Function File
            
            #region Case: Unknown State (Should never happen)
            else
            {
                Stop-PSFFunction -String 'Import-JeaScriptFile.UnknownError' -StringValues $file -Continue -EnableException $EnableException
            }
            #endregion Case: Unknown State (Should never happen)
        }
    }
}

function New-JeaCommand
{
<#
    .SYNOPSIS
        Creates a new command for use in a JEA Module's capability.
     
    .DESCRIPTION
        Creates a new command for use in a JEA Module's capability.
     
    .PARAMETER Name
        The name of the command.
     
    .PARAMETER Parameter
        Parameters to constrain.
        Specifying this will allow the end user to only use the thus listed parameters on the command.
        Valid input:
        - The string name of the parameter
        - A finished parameter object
        - A hashtable that contains further input value constraints. E.g.: @{ Name = 'Name'; ValidateSet = 'Dns', 'Spooler' }
     
    .PARAMETER Role
        A role to which to add the command.
        By default, the command object will just be returned by this function.
        If you specify a role, it will instead only be added to the role.
     
    .PARAMETER CommandType
        The type of command to add.
        Only applies when the command cannot be resolved.
        Defaults to function.
     
    .PARAMETER Force
        Override the security warning when generating an unsafe command.
        By default, New-JeaCommand will refuse to create a command object for commands deemed unsafe for use in JEA.
     
    .PARAMETER EnableException
        This parameters disables user-friendly warnings and enables the throwing of exceptions.
        This is less user friendly, but allows catching exceptions in calling scripts.
     
    .EXAMPLE
        PS C:\> New-JeaCommand -Name 'Restart-Service' -parameter 'Name'
 
        Generates a command object allowing the use of Get-Service, but only with the parameter "-Name"
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $Name,
        
        [JEAnalyzer.Parameter[]]
        $Parameter,
        
        [JEAnalyzer.Role]
        $Role,
        
        [System.Management.Automation.CommandTypes]
        $CommandType = [System.Management.Automation.CommandTypes]::Function,
        
        [switch]
        $Force,
        
        [switch]
        $EnableException
    )
    
    process
    {
        $commandData = Get-CommandMetaData -CommandName $Name
        # Eliminate Aliases
        if ($commandData.CommandObject.CommandType -eq 'Alias')
        {
            $commandData = Get-CommandMetaData -CommandName $commandData.CommandObject.ResolvedCommand.Name
        }
        if ($commandData.IsDangerous -and -not $Force)
        {
            Stop-PSFFunction -String 'New-JeaCommand.DangerousCommand' -StringValues $Name -EnableException $EnableException.ToBool() -Target $Name
            return
        }
        
        $resultCommand = New-Object -TypeName 'JEAnalyzer.CapabilityCommand' -Property @{
            Name = $commandData.CommandName
        }
        if ($commandData.CommandObject) { $resultCommand.CommandType = $commandData.CommandObject.CommandType }
        else { $resultCommand.CommandType = $CommandType }
        
        foreach ($parameterItem in $Parameter)
        {
            $resultCommand.Parameters[$parameterItem.Name] = $parameterItem
        }
        # Add to role if specified, otherwise return
        if ($Role) { $null = $Role.CommandCapability[$commandData.CommandName] = $resultCommand }
        else { $resultCommand }
    }
}

function New-JeaModule
{
<#
    .SYNOPSIS
        Creates a new JEA module object.
     
    .DESCRIPTION
        Used to create a JEA module object.
        This is the container used to add roles and resources that will later be used to generate a full JEA Module.
         
        Modules are created with an empty default role. Unless adding additional roles, all changes will be applied against the default role.
        To create a new role, use the New-JeaRole command.
         
        Use Export-JeaModule to convert this object into the full module.
     
    .PARAMETER Name
        The name of the JEA Module.
        Cannot coexist with other modules of the same name, the latest version will superseed older versions.
     
    .PARAMETER Identity
        Users or groups with permission to connect to an endpoint and receive the default role.
        If left empty, only remote management users will be able to connect to this endpoint.
        Either use AD Objects (such as the output of Get-ADGroup) or offer netbios-domain-qualified names as string.
     
    .PARAMETER Description
        A description for the module to be created.
     
    .PARAMETER Author
        The author that created the JEA Module.
        Controlled using the 'JEAnalyzer.Author' configuration setting.
     
    .PARAMETER Company
        The company the JEA Module was created for.
        Controlled using the 'JEAnalyzer.Company' configuration setting.
     
    .PARAMETER Version
        The version of the JEA Module.
        A higher version will superseed all older versions of the same name.
     
    .PARAMETER PreImport
        Scripts to execute during JEA module import, before loading functions.
        Offer either:
        - The path to the file to add
        - A hashtable with two keys: Name & Text
     
    .PARAMETER PostImport
        Scripts to execute during JEA module import, after loading functions.
        Offer either:
        - The path to the file to add
        - A hashtable with two keys: Name & Text
     
    .PARAMETER RequiredModules
        Any dependencies the module has.
        Note: Specify this in the same manner you would in a module manifest.
        Note2: Do not use this for modules you cannot publish in a repository if you want to distribute this JEA module in such.
        For example, taking a dependency on the Active Directory module would be disadvised.
        In this coses, instead import them as a PreImport-script.
     
    .EXAMPLE
        PS C:\> New-JeaModule -Name 'JEA_ADUser' -Description 'Grants access to the Get-ADUser command'
         
        Creates a JEA module object with the name JEA_ADUser.
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $Name,
        
        [string]
        $Identity,
        
        [string]
        $Description,
        
        [string]
        $Author = (Get-PSFConfigValue -FullName 'JEAnalyzer.Author'),
        
        [string]
        $Company = (Get-PSFConfigValue -FullName 'JEAnalyzer.Company'),
        
        [version]
        $Version = '1.0.0',
        
        [JEAnalyzer.ScriptFile[]]
        $PreImport,
        
        [JEAnalyzer.ScriptFile[]]
        $PostImport,
        
        [object]
        $RequiredModules
    )
    
    process
    {
        Write-PSFMessage -String 'New-JeaModule.Creating' -StringValues $Name, $Version
        $module = New-Object -TypeName JEAnalyzer.Module -Property @{
            Name        = $Name
            Description = $Description
            Version        = $Version
            Author        = $Author
            Company        = $Company
        }
        if ($Identity) { $module.Roles[$Name] = New-JeaRole -Name $Name -Identity $Identity }
        if ($RequiredModules) { $module.RequiredModules = $RequiredModules }
        foreach ($scriptFile in $PreImport) { $module.PreimportScripts[$scriptFile.Name] = $scriptFile }
        foreach ($scriptFile in $PostImport) { $module.PostimportScripts[$scriptFile.Name] = $scriptFile }
        
        $module
    }
}

function New-JeaRole
{
<#
    .SYNOPSIS
        Creates a new role for use in a JEA Module.
     
    .DESCRIPTION
        Creates a new role for use in a JEA Module.
     
        A role is a what maps a user or group identity to the resources it may use.
        Thus it consists of:
        - An Identity to apply to
        - Capabilities the user is granted.
        Capabilities can be any command or a custom script / command that will be embedded in the module.
     
    .PARAMETER Name
        The name of the role.
        On any given endpoint, all roles across ALL JEA Modules must have a unique name.
        To ensure this happens, all roles will automatically receive the modulename as prefix.
     
    .PARAMETER Identity
        Users or groups with permission to connect to an endpoint and receive this role.
        If left empty, only remote management users will be able to connect to this endpoint.
        Either use AD Objects (such as the output of Get-ADGroup) or offer netbios-domain-qualified names as string.
     
    .PARAMETER Capability
        The capabilities a role is supposed to have.
        This can be any kind of object - the name of a command, the output of Get-Command, path to a scriptfile or the output of any of the processing commands JEAnalyzer possesses (such as Read-JeaScriptFile).
     
    .PARAMETER Module
        A JEA module to which to add the role.
     
    .EXAMPLE
        PS C:\> New-JeaRole -Name 'Test'
     
        Creates an empty JEA Role named 'Test'
     
    .EXAMPLE
        PS C:\> New-JeaRole -Name 'Test' -Identity (Get-ADGroup JeaTestGroup)
     
        Creates an empty JEA Role named 'Test' that will grant remote access to members of the JeaTestGroup group.
     
    .EXAMPLE
        PS C:\> Read-JeaScriptFile -Path .\logon.ps1 | Where-Object CommandName -like "Get-AD*" | New-JeaRole -Name Logon -Identity (Get-ADGroup Domain-Users) | Add-JeaModuleRole -Module $module
     
        Parses the file logon.ps1 for commands.
        Then selects all of those commands that are used to read from Active Directory.
        It then creates a JEA Role named 'Logon', granting access to all AD Users to the commands selected.
        Finally, it adds the new role to the JEA Module object stored in $module.
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $Name,
        
        [Parameter(Mandatory = $true)]
        [string[]]
        $Identity,
        
        [Parameter(ValueFromPipeline = $true)]
        $Capability,
        
        [JEAnalyzer.Module]
        $Module
    )
    
    begin
    {
        Write-PSFMessage -String 'New-JeaRole.Creating' -StringValues $Name
        $role = New-Object -TypeName 'JEAnalyzer.Role' -ArgumentList $Name, $Identity
    }
    process
    {
        $Capability | ConvertTo-JeaCapability | ForEach-Object {
            $null = $role.CommandCapability[$_.Name] = $_
        }
    }
    end
    {
        if ($Module) { $Module.Roles[$role.Name] = $role }
        else { $role }
    }
}

function Read-JeaScriptblock
{
<#
    .SYNOPSIS
        Reads a scriptblock and returns qualified command objects of commands found.
     
    .DESCRIPTION
        Reads a scriptblock and returns qualified command objects of commands found.
     
    .PARAMETER ScriptCode
        The string version of the scriptcode to parse.
     
    .PARAMETER ScriptBlock
        A scriptblock to parse.
     
    .PARAMETER EnableException
        This parameters disables user-friendly warnings and enables the throwing of exceptions.
        This is less user friendly, but allows catching exceptions in calling scripts.
     
    .EXAMPLE
        PS C:\> Get-SBLEvent | Read-JeaScriptblock
     
        Scans the local computer for scriptblock logging events and parses out the commands they use.
#>

    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('Code')]
        [string[]]
        $ScriptCode,
        
        [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [System.Management.Automation.ScriptBlock[]]
        $ScriptBlock,
        
        [switch]
        $EnableException
    )
    
    begin
    {
        Write-PSFMessage -Level InternalComment -Message "Bound parameters: $($PSBoundParameters.Keys -join ", ")" -Tag 'debug', 'start', 'param'
        
        $fromPipeline = Test-PSFParameterBinding -ParameterName ScriptCode, ScriptBlock -Not
    }
    process
    {
        #region Processing Scriptblock strings
        foreach ($codeItem in $ScriptCode)
        {
            if ($codeItem -eq 'System.Management.Automation.ScriptBlock') { continue }
            if ($ScriptBlock -and $fromPipeline) { continue }
            
            # Never log the full scriptblock, it might contain sensitive information
            Write-PSFMessage -Level Verbose -Message "Processing a scriptblock with $($codeItem.Length) characters"
            try { $codeBlock = [System.Management.Automation.ScriptBlock]::Create($codeItem) }
            catch { Stop-PSFFunction -Message "Failed to parse text as scriptblock, skipping" -EnableException $EnableException -ErrorRecord $_ -OverrideExceptionMessage -Continue }
            $commands = (Read-Script -ScriptCode $codeBlock).Tokens | Where-Object TokenFlags -like "*CommandName*" | Group-Object Text | Select-Object -ExpandProperty Name | Where-Object { $_ }
            
            Write-PSFMessage -Level Verbose -Message "$($commands.Count) different commands found" -Target $pathItem
            
            if ($commands) { Get-CommandMetaData -CommandName $commands }
        }
        #endregion Processing Scriptblock strings
        
        #region Processing Scriptblocks
        foreach ($codeItem in $ScriptBlock)
        {
            # Never log the full scriptblock, it might contain sensitive information
            Write-PSFMessage -Level Verbose -Message "Processing a scriptblock with $($codeItem.ToString().Length) characters"
            $commands = (Read-Script -ScriptCode $codeItem).Tokens | Where-Object TokenFlags -like "*CommandName*" | Group-Object Text | Select-Object -ExpandProperty Name | Where-Object { $_ }
            
            Write-PSFMessage -Level Verbose -Message "$($commands.Count) different commands found" -Target $pathItem
            
            if ($commands) { Get-CommandMetaData -CommandName $commands }
        }
        #endregion Processing Scriptblocks
    }
}

function Read-JeaScriptFile
{
<#
    .SYNOPSIS
        Parses scriptfiles and returns qualified command objects of commands found.
     
    .DESCRIPTION
        Parses scriptfiles and returns qualified command objects of commands found.
     
        Note:
        The IsDangerous property is a best-effort thing.
        We TRY to find all dangerous commands, that might allow the user to escalate permissions on the Jea Endpoint.
        There is no guarantee for complete success however.
     
    .PARAMETER Path
        The path to scan.
        Will ignore folders, does not discriminate by extension.
     
    .PARAMETER EnableException
        This parameters disables user-friendly warnings and enables the throwing of exceptions.
        This is less user friendly, but allows catching exceptions in calling scripts.
     
    .EXAMPLE
        PS C:\> Get-ChildItem . -Filter *.ps1 -Recurse | Read-JeaScriptFile
     
        Scans all powershell script files in the folder and subfolder, then parses out command tokens.
#>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('FullName')]
        [string]
        $Path,
        
        [switch]
        $EnableException
    )
    
    begin
    {
        Write-PSFMessage -Level InternalComment -Message "Bound parameters: $($PSBoundParameters.Keys -join ", ")" -Tag 'debug', 'start', 'param'
        $filesProcessed = @()
    }
    process
    {
        foreach ($pathItem in $Path)
        {
            Write-PSFMessage -Level Verbose -Message "Processing $pathItem" -Target $pathItem
            try { $resolvedPaths = Resolve-PSFPath -Path $pathItem -Provider FileSystem }
            catch { Stop-PSFFunction -Message "Unable to resolve path: $pathItem" -Target $pathItem -EnableException $EnableException -Continue }
            foreach ($resolvedPath in $resolvedPaths)
            {
                $pathObject = Get-Item $resolvedPath
                
                if ($filesProcessed -contains $pathObject.FullName) { continue }
                if ($pathObject.PSIsContainer) { continue }
                
                $filesProcessed += $pathObject.FullName
                $commands = (Read-Script -Path $pathObject.FullName).Tokens | Where-Object TokenFlags -like "*CommandName*" | Group-Object Text | Select-Object -ExpandProperty Name | Where-Object { $_ }
                Write-PSFMessage -Level Verbose -Message "$($commands.Count) different commands found" -Target $pathItem
                    
                if ($commands) { Get-CommandMetaData -CommandName $commands -File $pathObject.FullName }
            }
        }
    }
}

function Test-JeaCommand
{
<#
    .SYNOPSIS
        Tests, whether a command is safe to expose in JEA.
     
    .DESCRIPTION
        Tests, whether a command is safe to expose in JEA.
        Unsafe commands allow escaping the lockdown that JEA is supposed to provide.
        Safety check is a best effort initiative and not an absolute determination.
     
    .PARAMETER Name
        Name of the command to test
     
    .EXAMPLE
        PS C:\> Test-JeaCommand -Name 'Get-Command'
     
        Tests whether Get-Command is safe to expose in JEA (Hint: It is)
#>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('CommandName')]
        [string[]]
        $Name
    )
    
    process
    {
        foreach ($commandName in $Name)
        {
            Get-CommandMetaData -CommandName $commandName
        }
    }
}

function Export-JeaModule
{
<#
    .SYNOPSIS
        Exports a JEA module object into a PowerShell Module.
     
    .DESCRIPTION
        Exports a JEA module object into a PowerShell Module.
        This will create a full PowerShell Module, including:
        - Role Definitions for all Roles
        - Command: Register-JeaEndpoint_<ModuleName> to register the session configuration.
        - Any additional commands and scripts required/contained by the Roles
        Create a JEA Module object using New-JeaModule
        Create roles by using New-JeaRole.
     
    .PARAMETER Path
        The folder where to place the module.
     
    .PARAMETER Module
        The module object to export.
     
    .PARAMETER Basic
        Whether the JEA module should be deployed as a basic/compatibility version.
        In that mode, it will not generate a version folder and target role capabilities by name rather than path.
        This is compatible with older operating systems but prevents simple deployment via package management.
     
    .EXAMPLE
        PS C:\> $module | Export-JeaModule -Path 'C:\temp'
     
        Exports the JEA Module stored in $module to the designated path.
#>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, Position = 0)]
        [PsfValidateScript('JEAnalyzer.ValidatePath.Directory', ErrorString = 'Validate.FileSystem.Directory.Fail')]
        [string]
        $Path,
        
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [JEAnalyzer.Module[]]
        $Module,
        
        [switch]
        $Basic
    )
    
    begin
    {
        #region Utility Functions
        function Write-Function
        {
        <#
            .SYNOPSIS
                Creates a function file with UTF8Bom encoding.
             
            .DESCRIPTION
                Creates a function file with UTF8Bom encoding.
             
            .PARAMETER Function
                The function object to write.
             
            .PARAMETER Path
                The path to writer it to
             
            .EXAMPLE
                PS C:\> Write-Function -Function (Get-Command mkdir) -Path C:\temp\mkdir.ps1
             
                Writes the function definition for mkdir (including function statement) to the specified path.
        #>

            [CmdletBinding()]
            param (
                [System.Management.Automation.FunctionInfo]
                $Function,
                
                [string]
                $Path
            )
            
            $functionString = @'
function {0}
{{
{1}
}}
'@
 -f $Function.Name, $Function.Definition.Trim("`n`r")
            $encoding = New-Object System.Text.UTF8Encoding($true)
            Write-PSFMessage -String 'Export-JeaModule.File.Create' -StringValues $Path -FunctionName Export-JeaModule
            [System.IO.File]::WriteAllText($Path, $functionString, $encoding)
        }
        
        function Write-File
        {
            [CmdletBinding()]
            param (
                [string]
                $Text,
                
                [string]
                $Path
            )
            $encoding = New-Object System.Text.UTF8Encoding($true)
            Write-PSFMessage -String 'Export-JeaModule.File.Create' -StringValues $Path -FunctionName Export-JeaModule
            [System.IO.File]::WriteAllText($Path, $Text, $encoding)
        }
        #endregion Utility Functions
        
        # Will succeede, as the validation scriptblock checks this first
        $resolvedPath = Resolve-PSFPath -Path $Path -Provider FileSystem -SingleItem
    }
    process
    {
        foreach ($moduleObject in $Module)
        {
            $moduleName = $moduleObject.Name -replace '\s', '_'
            if ($moduleName -notlike "JEA_*") { $moduleName = "JEA_{0}" -f $moduleName }
            
            #region Create Module folder
            if (Test-Path -Path (Join-Path $resolvedPath $moduleName))
            {
                $moduleBase = Get-Item -Path (Join-Path $resolvedPath $moduleName)
                Write-PSFMessage -String 'Export-JeaModule.Folder.ModuleBaseExists' -StringValues $moduleBase.FullName
            }
            else
            {
                $moduleBase = New-Item -Path $resolvedPath -Name $moduleName -ItemType Directory -Force
                Write-PSFMessage -String 'Export-JeaModule.Folder.ModuleBaseNew' -StringValues $moduleBase.FullName
            }
            if ($Basic)
            {
                $rootFolder = $moduleBase
            }
            else
            {
                Write-PSFMessage -String 'Export-JeaModule.Folder.VersionRoot' -StringValues $moduleBase.FullName, $moduleObject.Version
                $rootFolder = New-Item -Path $moduleBase.FullName -Name $moduleObject.Version -ItemType Directory -Force
            }
            
            # Other folders for the scaffold
            $folders = @(
                'functions'
                'internal\functions'
                'internal\scriptsPre'
                'internal\scriptsPost'
                'internal\scriptsRole'
            )
            foreach ($folder in $folders)
            {
                Write-PSFMessage -String 'Export-JeaModule.Folder.Content' -StringValues $folder
                $folderItem = New-Item -Path (Join-Path -Path $rootFolder.FullName -ChildPath $folder) -ItemType Directory -Force
                '# <Placeholder>' | Set-Content -Path "$($folderItem.FullName)\readme.md"
            }
            #endregion Create Module folder
            
            #region Create Role Capabilities
            Write-PSFMessage -String 'Export-JeaModule.Folder.RoleCapailities' -StringValues $rootFolder.FullName
            $roleCapabilityFolder = New-Item -Path $rootFolder.FullName -Name 'RoleCapabilities' -Force -ItemType Directory
            foreach ($role in $moduleObject.Roles.Values)
            {
                $RoleCapParams = @{
                    Path           = ('{0}\{1}.psrc' -f $roleCapabilityFolder.FullName, $role.Name)
                    Author           = $moduleObject.Author
                    CompanyName    = $moduleObject.Company
                    VisibleCmdlets = $role.VisibleCmdlets()
                    VisibleFunctions = $role.VisibleFunctions($moduleName)
                    ModulesToImport = $moduleName
                }
                Write-PSFMessage -String 'Export-JeaModule.Role.NewRole' -StringValues $role.Name, $role.CommandCapability.Count
                New-PSRoleCapabilityFile @RoleCapParams
                #region Logging Visible Commands
                foreach ($cmdlet in $role.VisibleCmdlets())
                {
                    $commandName = $cmdlet.Name
                    $parameters = @()
                    foreach ($parameter in $cmdlet.Parameters)
                    {
                        $string = $parameter.Name
                        if ($parameter.ValidateSet) { $string += (' | {0}' -f ($parameter.ValidateSet -join ",")) }
                        if ($parameter.ValidatePattern) { $string += (' | {0}' -f $parameter.ValidatePattern) }
                        $parameters += '({0})' -f $string
                    }
                    $parameterText = ' : {0}' -f ($parameters -join ",")
                    if (-not $parameters) { $parameterText = '' }
                    Write-PSFMessage -String 'Export-JeaModule.Role.VisibleCmdlet' -StringValues $role.Name, $commandName, $parameterText
                }
                foreach ($cmdlet in $role.VisibleFunctions($moduleName))
                {
                    $commandName = $cmdlet.Name
                    $parameters = @()
                    foreach ($parameter in $cmdlet.Parameters)
                    {
                        $string = $parameter.Name
                        if ($parameter.ValidateSet) { $string += (' | {0}' -f ($parameter.ValidateSet -join ",")) }
                        if ($parameter.ValidatePattern) { $string += (' | {0}' -f $parameter.ValidatePattern) }
                        $parameters += '({0})' -f $string
                    }
                    $parameterText = ' : {0}' -f ($parameters -join ",")
                    if (-not $parameters) { $parameterText = '' }
                    Write-PSFMessage -String 'Export-JeaModule.Role.VisibleFunction' -StringValues $role.Name, $commandName, $parameterText
                }
                #endregion Logging Visible Commands
                
                # Transfer all function definitions stored in the role.
                $role.CopyFunctionDefinitions($moduleObject)
            }
            #endregion Create Role Capabilities
            
            #region Create Private Functions
            $privateFunctionPath = Join-Path -Path $rootFolder.FullName -ChildPath 'internal\functions'
            foreach ($privateFunction in $moduleObject.PrivateFunctions.Values)
            {
                $outputPath = Join-Path -Path $privateFunctionPath -ChildPath "$($privateFunction.Name).ps1"
                Write-Function -Function $privateFunction -Path $outputPath
            }
            #endregion Create Private Functions
            
            #region Create Public Functions
            $publicFunctionPath = Join-Path -Path $rootFolder.FullName -ChildPath 'functions'
            foreach ($publicFunction in $moduleObject.PublicFunctions.Values)
            {
                $outputPath = Join-Path -Path $publicFunctionPath -ChildPath "$($publicFunction.Name).ps1"
                Write-Function -Function $publicFunction -Path $outputPath
            }
            #endregion Create Public Functions
            
            #region Create Scriptblocks
            foreach ($scriptFile in $moduleObject.PreimportScripts.Values)
            {
                Write-File -Text $scriptFile.Text -Path "$($rootFolder.FullName)\internal\scriptsPre\$($scriptFile.Name).ps1"
            }
            foreach ($scriptFile in $moduleObject.PostimportScripts.Values)
            {
                Write-File -Text $scriptFile.Text -Path "$($rootFolder.FullName)\internal\scriptsPost\$($scriptFile.Name).ps1"
            }
            #endregion Create Scriptblocks
            
            #region Create Common Resources
            # Register-JeaEndpoint
            $encoding = New-Object System.Text.UTF8Encoding($true)
            $functionText = [System.IO.File]::ReadAllText("$script:ModuleRoot\internal\resources\Register-JeaEndpointPublic.ps1", $encoding)
            $functionText = $functionText -replace 'Register-JeaEndpointPublic', "Register-JeaEndpoint_$($moduleName)"
            Write-File -Text $functionText -Path "$($rootFolder.FullName)\functions\Register-JeaEndpoint_$($moduleName).ps1"
            $functionText2 = [System.IO.File]::ReadAllText("$script:ModuleRoot\internal\resources\Register-JeaEndpoint.ps1", $encoding)
            Write-File -Text $functionText2 -Path "$($rootFolder.FullName)\internal\functions\Register-JeaEndpoint.ps1"
            
            # PSM1
            Copy-Item -Path "$script:ModuleRoot\internal\resources\jeamodule.psm1" -Destination "$($rootFolder.FullName)\$($moduleName).psm1"
            
            # PSSession Configuration
            $grouped = $moduleObject.Roles.Values | ForEach-Object {
                foreach ($identity in $_.Identity)
                {
                    [pscustomobject]@{
                        Identity = $identity
                        Role = $_
                    }
                }
            } | Group-Object Identity
            $roleDefinitions = @{ }
            foreach ($groupItem in $grouped)
            {
                if ($Basic)
                {
                    $roleDefinitions[$groupItem.Name] = @{
                        RoleCapabilities = $groupItem.Group.Role.Name
                    }
                }
                else
                {
                $roleDefinitions[$groupItem.Name] = @{
                        RoleCapabilityFiles = ($groupItem.Group.Role.Name | ForEach-Object { "C:\Program Files\WindowsPowerShell\Modules\{0}\{1}\RoleCapabilities\{2}.psrc" -f $moduleName, $Module.Version, $_ })
                    }
                }
            }
            $paramNewPSSessionConfigurationFile = @{
                SessionType = 'RestrictedRemoteServer'
                Path        = "$($rootFolder.FullName)\sessionconfiguration.pssc"
                RunAsVirtualAccount = $true
                RoleDefinitions = $roleDefinitions
                Author        = $moduleObject.Author
                Description = "[{0} {1}] {2}" -f $moduleName, $moduleObject.Version, $moduleObject.Description
                CompanyName = $moduleObject.Company
            }
            Write-PSFMessage -String 'Export-JeaModule.File.Create' -StringValues "$($rootFolder.FullName)\sessionconfiguration.pssc"
            New-PSSessionConfigurationFile @paramNewPSSessionConfigurationFile
            
            # Create Manifest
            $paramNewModuleManifest = @{
                FunctionsToExport = (Get-ChildItem -Path "$($rootFolder.FullName)\functions" -Filter '*.ps1').BaseName
                CmdletsToExport   = @()
                AliasesToExport   = @()
                VariablesToExport = @()
                Path              = "$($rootFolder.FullName)\$($moduleName).psd1"
                Author              = $moduleObject.Author
                Description          = $moduleObject.Description
                CompanyName          = $moduleObject.Company
                RootModule          = "$($moduleName).psm1"
                ModuleVersion      = $moduleObject.Version
                Tags              = 'JEA', 'JEAnalyzer', 'JEA_Module'
            }
            if ($moduleObject.RequiredModules) { $paramNewModuleManifest.RequiredModules = $moduleObject.RequiredModules }
            Write-PSFMessage -String 'Export-JeaModule.File.Create' -StringValues "$($rootFolder.FullName)\$($moduleName).psd1"
            New-ModuleManifest @paramNewModuleManifest
            #endregion Create Common Resources
            
            #region Generate Connection Script
            $connectionSegments = @()
            foreach ($role in $moduleObject.Roles.Values)
            {
                $connectionSegments += @'
# Connect to JEA Endpoint for Role {0}
$session = New-PSSession -ComputerName '<InsertNameHere>' -ConfigurationName '{1}'
Import-PSSession -AllowClobber -Session $session -DisableNameChecking -CommandName '{2}'
Invoke-Command -Session $session -Scriptblock {{ {3} }}
'@
 -f $role.Name, $moduleName, ($role.CommandCapability.Keys -join "', '"), ($role.CommandCapability.Keys | Select-Object -First 1)
            }
            
            $finalConnectionText = @'
<#
These are the connection scriptblocks for the {0} JEA Module.
For each role there is an entry with all that is needed to connect and consume it.
Just Copy&Paste the section you need, add it to the top of your script and insert the computername.
You will always need to create the session, but whether to Import it or use Invoke-Command is up to you.
Either option will work, importing it is usually more convenient but will overwrite local copies.
Invoke-Command is the better option if you want to connect to multiple such sessions or still need access to the local copies.
 
Note: If a user has access to multiple roles, you still only need one session, but:
- On Invoke-Command you have immediately access to ALL commands allowed in any role the user is in.
- On Import-PSSession, you need to explicitly state all the commands you want.
#>
 
{1}
'@
 -f $moduleName, ($connectionSegments -join "`n`n`n")
            Write-File -Text $finalConnectionText -Path (Join-Path -Path $resolvedPath -ChildPath "connect_$($moduleName).ps1")
            #endregion Generate Connection Script
        }
    }
}

function Export-JeaRoleCapFile
{
<#
    .SYNOPSIS
        Converts a list of commands into a JEA Role Capability File.
     
    .DESCRIPTION
        Converts a list of commands into a JEA Role Capability File.
     
        Accepts a list of input types, both from the output of other commands in the module and other calls legitimately pointing at a command.
        Then builds a Role Capability File that can be used to create a JEA Endpoint permitting use of the listed commands.
     
    .PARAMETER Path
        The path where to create the output file.
        If only a folder is specified, a 'configuration.psrc' will be created in that folder.
        If a filename is specified, it will use the name, adding the '.psrc' extension if necessary.
        The parent folder must exist, the file needs not exist (and will be overwritten if it does).
     
    .PARAMETER InputObject
        The commands to add to the list of allowed commands.
     
    .PARAMETER Author
        The author that created the RCF.
        Controlled using the 'JEAnalyzer.Author' configuration setting.
     
    .PARAMETER Company
        The company the RCF was created for.
        Controlled using the 'JEAnalyzer.Company' configuration setting.
     
    .PARAMETER EnableException
        This parameters disables user-friendly warnings and enables the throwing of exceptions.
        This is less user friendly, but allows catching exceptions in calling scripts.
     
    .EXAMPLE
        PS C:\> Get-Content commands.txt | Export-JeaRoleCapFile -Path '.\mytask.psrc'
     
        Creates a Jea Role Capability File permitting the use of all commands allowed in commands.txt.
#>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $Path,
        
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [Alias('CommandName','Name','Command')]
        [object[]]
        $InputObject,
        
        [string]
        $Author = (Get-PSFConfigValue -FullName 'JEAnalyzer.Author'),
        
        [string]
        $Company = (Get-PSFConfigValue -FullName 'JEAnalyzer.Company'),
        
        [switch]
        $EnableException
    )
    
    begin
    {
        #region Resolves the path
        try { $resolvedPath = Resolve-PSFPath -Path $Path -NewChild -Provider FileSystem -SingleItem -ErrorAction Stop }
        catch
        {
            Stop-PSFFunction -Message 'Failed to resolve output path' -ErrorRecord $_ -EnableException $EnableException
            return
        }
        if (Test-Path $resolvedPath)
        {
            $item = Get-Item -Path $resolvedPath
            if ($item.PSIsContainer) { $resolvedPath = Join-Path -Path $resolvedPath -ChildPath 'configuration.psrc' }
        }
        if ($resolvedPath -notlike '*.psrc') { $resolvedPath += '.psrc' }
        #endregion Resolves the path
        
        $commands = @()
    }
    process
    {
        if (Test-PSFFunctionInterrupt) { return }
        
        #region Add commands to list as they are received
        foreach ($item in $InputObject)
        {
            # Plain Names
            if ($item -is [string])
            {
                Write-PSFMessage -Level Verbose -Message "Adding command: $item" -Target $item
                $commands += $item
                continue
            }
            # Cmdlet objects
            if ($item -is [System.Management.Automation.CmdletInfo])
            {
                Write-PSFMessage -Level Verbose -Message "Adding command: $item" -Target $item
                $commands += $item.Name
                continue
            }
            # Function objects
            if ($item -is [System.Management.Automation.FunctionInfo])
            {
                Write-PSFMessage -Level Verbose -Message "Adding command: $item" -Target $item
                $commands += $item.Name
                continue
            }
            # Alias objects
            if ($item -is [System.Management.Automation.AliasInfo])
            {
                Write-PSFMessage -Level Verbose -Message "Adding command: $($item.ResolvedCommand.Name)" -Target $item
                $commands += $item.ResolvedCommand.Name
                continue
            }
            # Analyzer Objects
            if ($item.CommandName -is [string])
            {
                Write-PSFMessage -Level Verbose -Message "Adding command: $($item.CommandName)" -Target $item
                $commands += $item.CommandName
                continue
            }
            Stop-PSFFunction -Message "Failed to interpret as command: $item" -Target $item -Continue -EnableException $EnableException
        }
        #endregion Add commands to list as they are received
    }
    end
    {
        if (Test-PSFFunctionInterrupt) { return }
        
        #region Realize RCF
        if ($commands)
        {
            Write-PSFMessage -Level Verbose -Message "Creating Jea Role Capability File with $($commands.Count) commands permitted."
            $RoleCapParams = @{
                Path = $resolvedPath
                Author = $Author
                CompanyName = $Company
                VisibleCmdlets = ([string[]]($commands | Select-Object -Unique))
            }
            New-PSRoleCapabilityFile @RoleCapParams
        }
        else
        {
            Write-PSFMessage -Level Warning -Message 'No commands specified!'
        }
        #endregion Realize RCF
    }
}

function Install-JeaModule
{
<#
    .SYNOPSIS
        Installs a JEA module on a target endpoint.
     
    .DESCRIPTION
        Installs a JEA module on a target endpoint.
     
    .PARAMETER ComputerName
        The computers to install the module on
     
    .PARAMETER Credential
        The credentials to use for remoting
     
    .PARAMETER Module
        The module object(s) to export and install
        Generate a JEA module object using New-JeaModule
     
    .PARAMETER Basic
        Whether the JEA module should be deployed as a basic/compatibility version.
        In that mode, it will not generate a version folder and target role capabilities by name rather than path.
        This is compatible with older operating systems but prevents simple deployment via package management.
     
    .PARAMETER EnableException
        This parameters disables user-friendly warnings and enables the throwing of exceptions.
        This is less user friendly, but allows catching exceptions in calling scripts.
     
    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
     
    .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.
     
    .EXAMPLE
        PS C:\> Install-JeaModule -ComputerName dc1.contoso.com,dc2.contoso.com -Module $Module
     
        Installs the JEA module in $Module on dc1.contoso.com and dc2.contoso.com
#>

    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(Mandatory = $true, Position = 0)]
        [PSFComputer[]]
        $ComputerName,
        
        [PSCredential]
        $Credential,
        
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [JEAnalyzer.Module[]]
        $Module,
        
        [switch]
        $Basic,
        
        [switch]
        $EnableException
    )
    
    begin
    {
        $workingDirectory = New-Item -Path (Get-PSFPath -Name Temp) -Name "JEA_$(Get-Random)" -ItemType Directory -Force
        $credParam = $PSBoundParameters | ConvertTo-PSFHashtable -Include Credential
    }
    process
    {
        foreach ($moduleObject in $Module)
        {
            if (-not (Test-PSFShouldProcess -ActionString 'Install-JeaModule.Install' -Target ($ComputerName -join ", "))) { continue }
            
            Write-PSFMessage -String 'Install-JeaModule.Exporting.Module' -StringValues $moduleObject.Name
            Export-JeaModule -Path $workingDirectory.FullName -Module $moduleObject -Basic:$Basic
            $moduleName = "JEA_$($moduleObject.Name)"
            
            #region Establish Sessions
            Write-PSFMessage -String 'Install-JeaModule.Connecting.Sessions' -StringValues ($ComputerName -join ", ") -Target $ComputerName
            $sessions = New-PSSession -ComputerName $ComputerName -ErrorAction SilentlyContinue -ErrorVariable failedServers @credParam
            if ($failedServers)
            {
                if ($EnableException) { Stop-PSFFunction -String 'Install-JeaModule.Connections.Failed' -StringValues ($failedServers.TargetObject -join ", ") -Target $failedServers.TargetObject -EnableException $EnableException }
                foreach ($failure in $failedServers) { Write-PSFMessage -Level Warning -String 'Install-JeaModule.Connections.Failed' -StringValues $failure.TargetObject -ErrorRecord $_ -Target $failure.TargetObject }
            }
            if (-not $sessions)
            {
                Write-PSFMessage -Level Warning -String 'Install-JeaModule.Connections.NoSessions'
                return
            }
            #endregion Establish Sessions
            
            foreach ($session in $sessions)
            {
                Write-PSFMessage -String 'Install-JeaModule.Copying.Module' -StringValues $moduleObject.Name, $session.ComputerName -Target $session.ComputerName
                Copy-Item -Path "$($workingDirectory.FullName)\$moduleName" -Destination 'C:\Program Files\WindowsPowerShell\Modules' -Recurse -Force -ToSession $session
            }
            
            Write-PSFMessage -String 'Install-JeaModule.Installing.Module' -StringValues $moduleObject.Name -Target $sessions
            Invoke-Command -Session $sessions -ScriptBlock {
                Import-Module $using:moduleName
                $null = & (Get-Module $using:moduleName) { Register-JeaEndpoint -WarningAction SilentlyContinue }
            } -ErrorAction SilentlyContinue
            
            $sessions | Remove-PSSession -WhatIf:$false -Confirm:$false -ErrorAction Ignore
        }
    }
    end
    {
        Remove-Item -Path $workingDirectory.FullName -Force -Recurse -ErrorAction SilentlyContinue
    }
}

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

Set-PSFConfig -Module 'JEAnalyzer' -Name 'Author' -Value $env:USERNAME -Initialize -Validation 'string' -SimpleExport -Description 'The default author name to use when creating role capability files'
Set-PSFConfig -Module 'JEAnalyzer' -Name 'Company' -Value 'JEAnalyzer' -Initialize -Validation 'string' -SimpleExport -Description 'The default company name to use when creating role capability files'

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


Set-PSFScriptblock -Name 'JEAnalyzer.ValidatePath.Directory' -Scriptblock {
    Param ($Path)
    if (-not (Test-Path $Path)) { return $false }
    try { $null = Resolve-PSFPath -Path $Path -Provider FileSystem -SingleItem }
    catch { return $false }
    (Get-Item -Path $Path).PSIsContainer
}

Register-PSFTeppArgumentCompleter -Command Import-JeaScriptFile -Parameter Encoding -Name psframework-encoding

# List of potentially dangerous commands
$script:dangerousCommands = @(
    '%'
    'ForEach'
    'ForEach-Object'
    '?'
    'Where'
    'Where-Object'
    'iex'
    'Add-LocalGroupMember'
    'Add-ADGroupMember'
    'net'
    'net.exe'
    'dsadd'
    'dsadd.exe'
    'Start-Process'
    'New-Service'
    'Invoke-Item'
    'iwmi'
    'Invoke-WmiMethod'
    'Invoke-CimMethod'
    'Invoke-Expression'
    'Invoke-Command'
    'New-ScheduledTask'
    'Register-ScheduledJob'
    'Register-ScheduledTask'
    '*.ps1'
)

New-PSFLicense -Product 'JEAnalyzer' -Manufacturer 'miwiesne' -ProductVersion $script:ModuleVersion -ProductType Module -Name MIT -Version "1.0.0.0" -Date (Get-Date "2018-09-17") -Text @"
Copyright (c) 2018 miwiesne
 
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