functions/write/Export-JeaModule.ps1
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 } } } |