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 |