From 8e0121243cd921d45fe176b263d5e46b5f02eb8d Mon Sep 17 00:00:00 2001 From: Friedrich Weinmann Date: Fri, 2 Oct 2020 09:02:30 +0200 Subject: [PATCH 1/3] 1.2.10 --- JEAnalyzer/JEAnalyzer.psd1 | 21 ++-- JEAnalyzer/bin/JEAnalyzer.dll | Bin 9216 -> 9728 bytes JEAnalyzer/bin/JEAnalyzer.pdb | Bin 2632 -> 4208 bytes JEAnalyzer/bin/JEAnalyzer.xml | 18 +++ JEAnalyzer/changelog.md | 18 +++ .../construct/Add-JeaModuleScript.ps1 | 82 ++++++++++++++ .../functions/construct/New-JeaCommand.ps1 | 12 +- .../functions/construct/New-JeaModule.ps1 | 38 ++++++- .../functions/parsing/Test-JeaCommand.ps1 | 35 ++++++ .../functions/write/Export-JeaModule.ps1 | 44 ++++++-- .../functions/write/Install-JeaModule.ps1 | 103 ++++++++++++++++++ .../functions/Get-CommandMetaData.ps1 | 2 +- .../resources/Register-JeaEndpointPublic.ps1 | 25 +++++ JEAnalyzer/internal/resources/jeamodule.psm1 | 8 +- .../JEAnalyzer/JEAnalyzer/JEAnalyzer.csproj | 3 +- library/JEAnalyzer/JEAnalyzer/Module.cs | 5 + library/JEAnalyzer/JEAnalyzer/ScriptFile.cs | 33 ++++++ 17 files changed, 417 insertions(+), 30 deletions(-) create mode 100644 JEAnalyzer/changelog.md create mode 100644 JEAnalyzer/functions/construct/Add-JeaModuleScript.ps1 create mode 100644 JEAnalyzer/functions/parsing/Test-JeaCommand.ps1 create mode 100644 JEAnalyzer/functions/write/Install-JeaModule.ps1 create mode 100644 JEAnalyzer/internal/resources/Register-JeaEndpointPublic.ps1 diff --git a/JEAnalyzer/JEAnalyzer.psd1 b/JEAnalyzer/JEAnalyzer.psd1 index ab578d8..9a2f1a2 100644 --- a/JEAnalyzer/JEAnalyzer.psd1 +++ b/JEAnalyzer/JEAnalyzer.psd1 @@ -4,16 +4,16 @@ # Version number of this module. - ModuleVersion = '1.1.0' + ModuleVersion = '1.2.10' # ID used to uniquely identify this module GUID = '346caa76-534a-4651-88f5-359e85cd71c0' # Author of this module - Author = 'Miriam Wiesner' + Author = 'Miriam Wiesner, Friedrich Weinmann' # Company or vendor of this module - CompanyName = 'Miriam Wiesner' + CompanyName = ' ' # Copyright statement for this module Copyright = 'Copyright (c) 2018 Miriam Wiesner' @@ -27,7 +27,7 @@ # Modules that must be imported into the global environment prior to importing # this module RequiredModules = @( - @{ ModuleName = 'PSFramework'; ModuleVersion = '1.0.12' } + @{ ModuleName = 'PSFramework'; ModuleVersion = '1.4.150' } ) # Assemblies that must be loaded prior to importing this module @@ -42,25 +42,28 @@ # Functions to export from this module FunctionsToExport = @( 'Add-JeaModuleRole' + 'Add-JeaModuleScript' 'ConvertTo-JeaCapability' + 'Export-JeaModule' + 'Export-JeaRoleCapFile' 'Import-JeaScriptFile' + 'Install-JeaModule' 'New-JeaCommand' 'New-JeaModule' 'New-JeaRole' 'Read-JeaScriptblock' 'Read-JeaScriptFile' - 'Export-JeaModule' - 'Export-JeaRoleCapFile' + 'Test-JeaCommand' ) # Cmdlets to export from this module - CmdletsToExport = '' + # CmdletsToExport = '' # Variables to export from this module - VariablesToExport = '' + # VariablesToExport = '' # Aliases to export from this module - AliasesToExport = '' + # AliasesToExport = '' # List of all modules packaged with this module ModuleList = @() diff --git a/JEAnalyzer/bin/JEAnalyzer.dll b/JEAnalyzer/bin/JEAnalyzer.dll index 85fa08716619816bbe860d596e843356b5eeef12..2b659dd9ca8dc365bc18a0278e6da7877bc603b6 100644 GIT binary patch delta 2921 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≦N_2jvSa-9@R#XE>EfSV@Ts?o;v z2U=3VEF7Uws$dYhv4D^0VU!leKoPqhrHbv2+rudBAJP3P7QR&vqs-GRq*=J7sgKJr z%0_LNR>Nj(H`?9WVU%lbpJGrD{hp12tBRM_L4XhPUXTQDpw!pkkF94i4eZs?Bj=xs zEdJ9_v14oBlFQnX@-05ysx8Dc3Ts~)< zlgF8iW=jEQ&dL{re5b{16mlI6IR*K7mb?OsBPTaE$5~*`bGaG{=He0b3mweb-u^oC1(TU#*Rn^6KQbMCYxB;0mp_=~p#2V;V!HF=K1$QR@rv0o`EgCr%; z;JRSBHhE!k$(1 zULojqiV9mID|KW4ySR6`V?eg`T>5_H3mKQ+RoM31Pk^h&7@7Adk3@-0?;){j*xjU$ z{2&-h+E3|Ejazy6h7!Bn>rseh-T&H{gBUD61MJ7=ZV=SA^Pfb}Bsf(i-7%j=n0wzZ zWkhdME!y!_;y^Oe!|8*noD%tT8)@-qhIr9^oFLtrdr9 z-F!+;TzqNeIOWi4gg;7Y342{o;9cbP)$rm5agwsfwpS@EE@GtGdZ@NOeYG>_4oKv! z-D9D)&kbi=KR&mhcI$ILo|Lxz?x~CY&v)&dHgbH*gMI9E`O5w)3tlHbb`#U_r-4$D zrpZ7iHqi80iGezm_Cc7bg7Gj{1sbSP!E9tUvD9QLd}J!dW|HKE1H8lSaZ62i)h9>X?X8Bdcm1{R;81U!YX7Y z9!dEnLrqiQUb}1`G;H`F7JbiDFr{z%T&wyiXO76)PER;vxw?7V(bYwkr)ScB$9lGi zG(i(bnpeC5$=HFim)`EV^Ea9~3uof2q9}MBo+hh=e^eYHNf5E@Nq0QP4t+;T$7I8w zukX!W#eF}ZJDAq#buN3{(pB~4hR<*(mY6uxUq>* z5Fv}oSty87L~!ROpc|=*n<9wSMR8T)wzzQPYv+GLtPwoiZ{|Pe%*?qnU;9?WOGD*~ z0{Rz$v<<{Som?ng&R?3RM2%o)@CfUIb-^5T-TAP)>{6KM z9ug!bC`2@CK49uO5zLB(eoHWDSr~Xscq#R`v zAwFuB>C-IJo;0W6v~K=?0(3LdLWfCe)6zN-B%AXTge)^lf#Tm8-);Nom!Xn)j|LR> zNS`R`Y1(hH+|-SxeZ_{~sRnJp9#IN!?dhhi(2hD4Wh3q^Q)f+s#hKDfPVaCAI3b;K zj_A9-3C|}YKG`Y5`nzwx-%F*=r?XeG*+qTWTM798Y_G?4m;XuBWiUfKj}~&dnS45t zpDhetoZpJ7hV^HE=5JQ@wmu!m>t*nlT!d#|3`et9<4jmMq8CUP*ar_42yMq+~ diff --git a/JEAnalyzer/bin/JEAnalyzer.xml b/JEAnalyzer/bin/JEAnalyzer.xml index 9c7a263..403cb5d 100644 --- a/JEAnalyzer/bin/JEAnalyzer.xml +++ b/JEAnalyzer/bin/JEAnalyzer.xml @@ -296,5 +296,23 @@ Content of the file + + + Create an empty file object + + + + + Create a file object based on an existing file + + Path to the file to load + + + + Create a file object from provided values + + Name of the file (should not include an extension) + The text content of the file + diff --git a/JEAnalyzer/changelog.md b/JEAnalyzer/changelog.md new file mode 100644 index 0000000..8d757ec --- /dev/null +++ b/JEAnalyzer/changelog.md @@ -0,0 +1,18 @@ +# Changelog + +## 1.2.10 + +- New: Command Install-JeaModule - Installs a JEA module on the target computer +- New: Command Add-JeaModuleScript - Adds a script to a JEA module +- New: Command Test-JeaCommand - Test an individual command for safety to publish in an endpoint. +- Upd: New-JeaModule - Added parameters for PreImport and PostImport scripts +- Upd: New-JeaModule - New parameter `-RequiredModules` enables specifying prerequisites +- Upd: New-JeaCommand - New parameter: `-CommandType` allows picking the type of command for unresolveable commands. +- Upd: JeaModules - all roles will now automatically import the jea module, irrespective of commands used +- Fix: Export-JeaModule - Does not write preimport and postimport scripts +- Fix: New-JeaCommand - Fails for unknown commands +- Fix: Export-JeaModule - New JEA modules will only try to load ps1 files on import. + +## 1.1.0 (???) + +- Pre-History \ No newline at end of file diff --git a/JEAnalyzer/functions/construct/Add-JeaModuleScript.ps1 b/JEAnalyzer/functions/construct/Add-JeaModuleScript.ps1 new file mode 100644 index 0000000..d71707d --- /dev/null +++ b/JEAnalyzer/functions/construct/Add-JeaModuleScript.ps1 @@ -0,0 +1,82 @@ +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 } + } + } +} \ No newline at end of file diff --git a/JEAnalyzer/functions/construct/New-JeaCommand.ps1 b/JEAnalyzer/functions/construct/New-JeaCommand.ps1 index b3ea4ee..c68e0cf 100644 --- a/JEAnalyzer/functions/construct/New-JeaCommand.ps1 +++ b/JEAnalyzer/functions/construct/New-JeaCommand.ps1 @@ -23,6 +23,11 @@ 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. @@ -49,6 +54,9 @@ [JEAnalyzer.Role] $Role, + [System.Management.Automation.CommandTypes] + $CommandType = [System.Management.Automation.CommandTypes]::Function, + [switch] $Force, @@ -72,8 +80,10 @@ $resultCommand = New-Object -TypeName 'JEAnalyzer.CapabilityCommand' -Property @{ Name = $commandData.CommandName - CommandType = $commandData.CommandObject.CommandType } + if ($commandData.CommandObject) { $resultCommand.CommandType = $commandData.CommandObject.CommandType } + else { $resultCommand.CommandType = $CommandType } + foreach ($parameterItem in $Parameter) { $resultCommand.Parameters[$parameterItem.Name] = $parameterItem diff --git a/JEAnalyzer/functions/construct/New-JeaModule.ps1 b/JEAnalyzer/functions/construct/New-JeaModule.ps1 index bb7f18d..5fdeda4 100644 --- a/JEAnalyzer/functions/construct/New-JeaModule.ps1 +++ b/JEAnalyzer/functions/construct/New-JeaModule.ps1 @@ -37,6 +37,25 @@ 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' @@ -62,13 +81,18 @@ $Company = (Get-PSFConfigValue -FullName 'JEAnalyzer.Company'), [version] - $Version = '1.0.0' + $Version = '1.0.0', + + [JEAnalyzer.ScriptFile[]] + $PreImport, + + [JEAnalyzer.ScriptFile[]] + $PostImport, + + [object] + $RequiredModules ) - begin - { - Write-PSFMessage -Level InternalComment -String 'General.BoundParameters' -StringValues ($PSBoundParameters.Keys -join ", ") -Tag 'debug', 'start', 'param' - } process { Write-PSFMessage -String 'New-JeaModule.Creating' -StringValues $Name, $Version @@ -80,6 +104,10 @@ 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 } } \ No newline at end of file diff --git a/JEAnalyzer/functions/parsing/Test-JeaCommand.ps1 b/JEAnalyzer/functions/parsing/Test-JeaCommand.ps1 new file mode 100644 index 0000000..c5eb920 --- /dev/null +++ b/JEAnalyzer/functions/parsing/Test-JeaCommand.ps1 @@ -0,0 +1,35 @@ +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 + } + } +} \ No newline at end of file diff --git a/JEAnalyzer/functions/write/Export-JeaModule.ps1 b/JEAnalyzer/functions/write/Export-JeaModule.ps1 index 059826d..b281bdc 100644 --- a/JEAnalyzer/functions/write/Export-JeaModule.ps1 +++ b/JEAnalyzer/functions/write/Export-JeaModule.ps1 @@ -19,6 +19,11 @@ .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' @@ -33,7 +38,10 @@ [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [JEAnalyzer.Module[]] - $Module + $Module, + + [switch] + $Basic ) begin @@ -116,8 +124,15 @@ function {0} $moduleBase = New-Item -Path $resolvedPath -Name $moduleName -ItemType Directory -Force Write-PSFMessage -String 'Export-JeaModule.Folder.ModuleBaseNew' -StringValues $moduleBase.FullName } - Write-PSFMessage -String 'Export-JeaModule.Folder.VersionRoot' -StringValues $moduleBase.FullName, $moduleObject.Version - $rootFolder = New-Item -Path $moduleBase.FullName -Name $moduleObject.Version -ItemType Directory -Force + 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 = @( @@ -146,6 +161,7 @@ function {0} 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 @@ -206,11 +222,11 @@ function {0} #endregion Create Public Functions #region Create Scriptblocks - foreach ($scriptFile in $moduleObject.PreimportScripts.Value) + foreach ($scriptFile in $moduleObject.PreimportScripts.Values) { Write-File -Text $scriptFile.Text -Path "$($rootFolder.FullName)\internal\scriptsPre\$($scriptFile.Name).ps1" } - foreach ($scriptFile in $moduleObject.PostimportScripts.Value) + foreach ($scriptFile in $moduleObject.PostimportScripts.Values) { Write-File -Text $scriptFile.Text -Path "$($rootFolder.FullName)\internal\scriptsPost\$($scriptFile.Name).ps1" } @@ -219,9 +235,11 @@ function {0} #region Create Common Resources # Register-JeaEndpoint $encoding = New-Object System.Text.UTF8Encoding($true) - $functionText = [System.IO.File]::ReadAllText("$script:ModuleRoot\internal\resources\Register-JeaEndpoint.ps1", $encoding) - $functionText = $functionText -replace 'Register-JeaEndpoint', "Register-JeaEndpoint_$($moduleName)" + $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" @@ -239,8 +257,17 @@ function {0} $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, $_ }) + RoleCapabilityFiles = ($groupItem.Group.Role.Name | ForEach-Object { "C:\Program Files\WindowsPowerShell\Modules\{0}\{1}\RoleCapabilities\{2}.psrc" -f $moduleName, $Module.Version, $_ }) + } } } $paramNewPSSessionConfigurationFile = @{ @@ -269,6 +296,7 @@ function {0} 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 diff --git a/JEAnalyzer/functions/write/Install-JeaModule.ps1 b/JEAnalyzer/functions/write/Install-JeaModule.ps1 new file mode 100644 index 0000000..136666f --- /dev/null +++ b/JEAnalyzer/functions/write/Install-JeaModule.ps1 @@ -0,0 +1,103 @@ +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. + + .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 + } +} \ No newline at end of file diff --git a/JEAnalyzer/internal/functions/Get-CommandMetaData.ps1 b/JEAnalyzer/internal/functions/Get-CommandMetaData.ps1 index dddae64..af193f9 100644 --- a/JEAnalyzer/internal/functions/Get-CommandMetaData.ps1 +++ b/JEAnalyzer/internal/functions/Get-CommandMetaData.ps1 @@ -47,9 +47,9 @@ Write-PSFMessage -Level Verbose -Message "Adding meta information for: $($command)" $commandObject = New-Object -TypeName 'JEAnalyzer.CommandInfo' -Property @{ CommandName = $command - CommandObject = $script:allcommands | Where-Object Name -EQ $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 diff --git a/JEAnalyzer/internal/resources/Register-JeaEndpointPublic.ps1 b/JEAnalyzer/internal/resources/Register-JeaEndpointPublic.ps1 new file mode 100644 index 0000000..bd2d95d --- /dev/null +++ b/JEAnalyzer/internal/resources/Register-JeaEndpointPublic.ps1 @@ -0,0 +1,25 @@ +function Register-JeaEndpointPublic +{ +<# + .SYNOPSIS + Registers the module's JEA session configuration in WinRM. + + .DESCRIPTION + Registers the module's JEA session configuration in WinRM. + This effectively enables the module as a remoting endpoint. + + .EXAMPLE + PS C:\> Register-JeaEndpointPublic + + Register this module in WinRM as a remoting target. +#> + [CmdletBinding()] + param ( + + ) + + process + { + Register-JeaEndpoint + } +} \ No newline at end of file diff --git a/JEAnalyzer/internal/resources/jeamodule.psm1 b/JEAnalyzer/internal/resources/jeamodule.psm1 index ec388f1..2263988 100644 --- a/JEAnalyzer/internal/resources/jeamodule.psm1 +++ b/JEAnalyzer/internal/resources/jeamodule.psm1 @@ -1,20 +1,20 @@ $script:ModuleRoot = $PSScriptRoot -foreach ($scriptFile in (Get-ChildItem -Path "$($script:ModuleRoot)\internal\scriptsPre\" -Recurse)) +foreach ($scriptFile in (Get-ChildItem -Path "$($script:ModuleRoot)\internal\scriptsPre\" -Recurse -Filter *.ps1)) { . $scriptFile.FullName } -foreach ($functionFile in (Get-ChildItem -Path "$($script:ModuleRoot)\internal\functions\" -Recurse)) +foreach ($functionFile in (Get-ChildItem -Path "$($script:ModuleRoot)\internal\functions\" -Recurse -Filter *.ps1)) { . $functionFile.FullName } -foreach ($functionFile in (Get-ChildItem -Path "$($script:ModuleRoot)\functions\" -Recurse)) +foreach ($functionFile in (Get-ChildItem -Path "$($script:ModuleRoot)\functions\" -Recurse -Filter *.ps1)) { . $functionFile.FullName } -foreach ($scriptFile in (Get-ChildItem -Path "$($script:ModuleRoot)\internal\scriptsPost\" -Recurse)) +foreach ($scriptFile in (Get-ChildItem -Path "$($script:ModuleRoot)\internal\scriptsPost\" -Recurse -Filter *.ps1)) { . $scriptFile.FullName } \ No newline at end of file diff --git a/library/JEAnalyzer/JEAnalyzer/JEAnalyzer.csproj b/library/JEAnalyzer/JEAnalyzer/JEAnalyzer.csproj index f4f1d67..8275fe8 100644 --- a/library/JEAnalyzer/JEAnalyzer/JEAnalyzer.csproj +++ b/library/JEAnalyzer/JEAnalyzer/JEAnalyzer.csproj @@ -20,9 +20,8 @@ - ..\..\..\..\..\..\Program Files\WindowsPowerShell\Modules\PSFramework\1.0.12\bin\PSFramework.dll + ..\..\..\..\psframework\PSFramework\bin\PSFramework.dll false - false ..\..\..\..\..\..\Windows\Microsoft.NET\assembly\GAC_MSIL\System.Management.Automation\v4.0_3.0.0.0__31bf3856ad364e35\System.Management.Automation.dll diff --git a/library/JEAnalyzer/JEAnalyzer/Module.cs b/library/JEAnalyzer/JEAnalyzer/Module.cs index fd15596..e23bc22 100644 --- a/library/JEAnalyzer/JEAnalyzer/Module.cs +++ b/library/JEAnalyzer/JEAnalyzer/Module.cs @@ -35,6 +35,11 @@ public class Module /// public string Company; + /// + /// Modules required for this module + /// + public object RequiredModules; + /// /// The roles contained in the module /// diff --git a/library/JEAnalyzer/JEAnalyzer/ScriptFile.cs b/library/JEAnalyzer/JEAnalyzer/ScriptFile.cs index 889e21f..07bbdd4 100644 --- a/library/JEAnalyzer/JEAnalyzer/ScriptFile.cs +++ b/library/JEAnalyzer/JEAnalyzer/ScriptFile.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Management.Automation; using System.Text; using System.Threading.Tasks; @@ -21,5 +23,36 @@ public class ScriptFile /// Content of the file /// public string Text; + + /// + /// Create an empty file object + /// + public ScriptFile() + { + + } + + /// + /// Create a file object based on an existing file + /// + /// Path to the file to load + public ScriptFile(string Path) + { + string resolvedPath = (new SessionState()).Path.GetResolvedPSPathFromPSPath(Path)[0].Path; + Text = File.ReadAllText(resolvedPath); + FileInfo tempInfo = new FileInfo(resolvedPath); + Name = tempInfo.Name.Substring(0, tempInfo.Name.Length - tempInfo.Extension.Length); + } + + /// + /// Create a file object from provided values + /// + /// Name of the file (should not include an extension) + /// The text content of the file + public ScriptFile(string Name, string Text) + { + this.Name = Name; + this.Text = Text; + } } } From 2461528314886eb9965def21d565d35e2e9073cd Mon Sep 17 00:00:00 2001 From: Friedrich Weinmann Date: Fri, 2 Oct 2020 09:17:18 +0200 Subject: [PATCH 2/3] test updates --- JEAnalyzer/en-us/strings.psd1 | 72 ++++----- .../tests/general/FileIntegrity.Tests.ps1 | 97 ++++++------ JEAnalyzer/tests/general/Help.Tests.ps1 | 149 ++++++------------ .../tests/general/PSScriptAnalyzer.Tests.ps1 | 18 +-- .../tests/general/strings.Exceptions.ps1 | 19 +++ JEAnalyzer/tests/general/strings.Tests.ps1 | 25 +++ JEAnalyzer/tests/pester.ps1 | 96 ++++++----- 7 files changed, 245 insertions(+), 231 deletions(-) create mode 100644 JEAnalyzer/tests/general/strings.Exceptions.ps1 create mode 100644 JEAnalyzer/tests/general/strings.Tests.ps1 diff --git a/JEAnalyzer/en-us/strings.psd1 b/JEAnalyzer/en-us/strings.psd1 index 8974101..0459f59 100644 --- a/JEAnalyzer/en-us/strings.psd1 +++ b/JEAnalyzer/en-us/strings.psd1 @@ -1,39 +1,37 @@ @{ - # General - 'General.BoundParameters' = 'Bound parameters: {0}' - - # Validation - 'Validate.FileSystem.Directory.Fail' = 'The input object could not be identified as a directory: {0}' - - # Assembly - 'Assembly.Parameter.MissingName' = 'Could not convert from hashtable, must contain a "Name" element!' - - # Command Add-JeaModuleRole - 'Add-JeaModuleRole.RolePresent' = 'Role {0} already exists in {1}! Use -Force to replace the existing role.' - 'Add-JeaModuleRole.AddingRole' = 'Adding role {0} to module {1}' - - # Command ConvertTo-Capability - 'ConvertTo-Capability.CapabilityNotKnown' = 'Could not convert to capability: {0}' - - # Command Export-JeaModule - 'Export-JeaModule.Folder.ModuleBaseExists' = "The module's base folder already exists: {0}" - 'Export-JeaModule.Folder.ModuleBaseNew' = 'Creating new module folder: {0}' - 'Export-JeaModule.Folder.VersionRoot' = 'Creating version specific module path: {0}\{1}' - 'Export-JeaModule.Folder.Content' = 'Creating subfolder: {0}' - 'Export-JeaModule.Folder.RoleCapailities' = 'Creating the folder to store Role Capability Files: {0}\RoleCapabilities' - 'Export-JeaModule.Role.NewRole' = 'Creating new Role: {0} ({1} Published Command Capabilities)' - 'Export-JeaModule.Role.VisibleCmdlet' = '[Role: {0}] Adding visible Cmdlet: {1}{2}' - 'Export-JeaModule.Role.VisibleFunction' = '[Role: {0}] Adding visible Function: {1}{2}' - 'Export-JeaModule.File.Create' = 'Creating File: {0}' - - # Command Import-JeaScriptFile - 'Import-JeaScriptFile.ProcessingInput' = 'Processing file for import: {0}' - 'Import-JeaScriptFile.ParsingError' = 'Parsing error for file: {0}' - 'Import-JeaScriptFile.UnknownError' = 'Unknown error when processing file: {0}' - - # Command New-JeaModule - 'New-JeaModule.Creating' = 'Creating JEA Module object for: {0} (v{1})' - - # Command New-JeaRole - 'New-JeaRole.Creating' = 'Creating Role: {0}' + 'Add-JeaModuleRole.AddingRole' = 'Adding role {0} to module {1}' # $roleItem.Name, $Module.Name + 'Add-JeaModuleRole.RolePresent' = 'Role {0} already exists in {1}! Use -Force to replace the existing role.' # $roleItem.Name, $Module.Name + + 'ConvertTo-Capability.CapabilityNotKnown' = 'Could not convert to capability: {0}' # $inputItem + + 'Export-JeaModule.File.Create' = 'Creating File: {0}' # $Path + 'Export-JeaModule.Folder.Content' = 'Creating subfolder: {0}' # $folder + 'Export-JeaModule.Folder.ModuleBaseExists' = "The module's base folder already exists: {0}" # $moduleBase.FullName + 'Export-JeaModule.Folder.ModuleBaseNew' = 'Creating new module folder: {0}' # $moduleBase.FullName + 'Export-JeaModule.Folder.RoleCapailities' = 'Creating the folder to store Role Capability Files: {0}\RoleCapabilities' # $rootFolder.FullName + 'Export-JeaModule.Folder.VersionRoot' = 'Creating version specific module path: {0}\{1}' # $moduleBase.FullName, $moduleObject.Version + 'Export-JeaModule.Role.NewRole' = 'Creating new Role: {0} ({1} Published Command Capabilities)' # $role.Name, $role.CommandCapability.Count + 'Export-JeaModule.Role.VisibleCmdlet' = '[Role: {0}] Adding visible Cmdlet: {1}{2}' # $role.Name, $commandName, $parameterText + 'Export-JeaModule.Role.VisibleFunction' = '[Role: {0}] Adding visible Function: {1}{2}' # $role.Name, $commandName, $parameterText + + 'FileSystem.Directory.Fail' = 'Not a directory: {0}' # , + + 'General.BoundParameters' = 'Bound parameters: {0}' # ($PSBoundParameters.Keys -join ", ") + + 'Import-JeaScriptFile.ParsingError' = 'Parsing error for file: {0}' # $file + 'Import-JeaScriptFile.ProcessingInput' = 'Processing file for import: {0}' # $file + 'Import-JeaScriptFile.UnknownError' = 'Unknown error when processing file: {0}' # $file + + 'Install-JeaModule.Connecting.Sessions' = 'Connecting via WinRM to {0}' # ($ComputerName -join ", ") + 'Install-JeaModule.Connections.Failed' = 'Failed to connect to {0}' # ($failedServers.TargetObject -join ", ") + 'Install-JeaModule.Connections.NoSessions' = 'No successful sessions established, terminating.' # + 'Install-JeaModule.Copying.Module' = 'Copying JEA module {0} to {1}' # $moduleObject.Name, $session.ComputerName + 'Install-JeaModule.Exporting.Module' = 'Exporting JEA module {0}' # $moduleObject.Name + 'Install-JeaModule.Installing.Module' = 'Installing JEA module {0}' # $moduleObject.Name + + 'New-JeaCommand.DangerousCommand' = 'Dangerous command detected: {0}. Interrupting, use "-Force" to accept insecure commands.' # $Name + + 'New-JeaModule.Creating' = 'Creating JEA Module object for: {0} (v{1})' # $Name, $Version + + 'New-JeaRole.Creating' = 'Creating Role: {0}' # $Name } \ No newline at end of file diff --git a/JEAnalyzer/tests/general/FileIntegrity.Tests.ps1 b/JEAnalyzer/tests/general/FileIntegrity.Tests.ps1 index ada453e..89e6c9c 100644 --- a/JEAnalyzer/tests/general/FileIntegrity.Tests.ps1 +++ b/JEAnalyzer/tests/general/FileIntegrity.Tests.ps1 @@ -1,49 +1,58 @@ -$moduleRoot = (Resolve-Path "$PSScriptRoot\..\..").Path +$moduleRoot = (Resolve-Path "$global:testroot\..").Path -. "$PSScriptRoot\FileIntegrity.Exceptions.ps1" - -function Get-FileEncoding -{ -<# - .SYNOPSIS - Tests a file for encoding. - - .DESCRIPTION - Tests a file for encoding. - - .PARAMETER Path - The file to test -#> - [CmdletBinding()] - Param ( - [Parameter(Mandatory = $True, ValueFromPipelineByPropertyName = $True)] - [Alias('FullName')] - [string] - $Path - ) - - [byte[]]$byte = get-content -Encoding byte -ReadCount 4 -TotalCount 4 -Path $Path - - if ($byte[0] -eq 0xef -and $byte[1] -eq 0xbb -and $byte[2] -eq 0xbf) { 'UTF8' } - elseif ($byte[0] -eq 0xfe -and $byte[1] -eq 0xff) { 'Unicode' } - elseif ($byte[0] -eq 0 -and $byte[1] -eq 0 -and $byte[2] -eq 0xfe -and $byte[3] -eq 0xff) { 'UTF32' } - elseif ($byte[0] -eq 0x2b -and $byte[1] -eq 0x2f -and $byte[2] -eq 0x76) { 'UTF7' } - else { 'Unknown' } -} +. "$global:testroot\general\FileIntegrity.Exceptions.ps1" Describe "Verifying integrity of module files" { + BeforeAll { + function Get-FileEncoding + { + <# + .SYNOPSIS + Tests a file for encoding. + + .DESCRIPTION + Tests a file for encoding. + + .PARAMETER Path + The file to test + #> + [CmdletBinding()] + Param ( + [Parameter(Mandatory = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('FullName')] + [string] + $Path + ) + + if ($PSVersionTable.PSVersion.Major -lt 6) + { + [byte[]]$byte = get-content -Encoding byte -ReadCount 4 -TotalCount 4 -Path $Path + } + else + { + [byte[]]$byte = Get-Content -AsByteStream -ReadCount 4 -TotalCount 4 -Path $Path + } + + if ($byte[0] -eq 0xef -and $byte[1] -eq 0xbb -and $byte[2] -eq 0xbf) { 'UTF8 BOM' } + elseif ($byte[0] -eq 0xfe -and $byte[1] -eq 0xff) { 'Unicode' } + elseif ($byte[0] -eq 0 -and $byte[1] -eq 0 -and $byte[2] -eq 0xfe -and $byte[3] -eq 0xff) { 'UTF32' } + elseif ($byte[0] -eq 0x2b -and $byte[1] -eq 0x2f -and $byte[2] -eq 0x76) { 'UTF7' } + else { 'Unknown' } + } + } + Context "Validating PS1 Script files" { - $allFiles = Get-ChildItem -Path $moduleRoot -Recurse -Filter "*.ps1" | Where-Object FullName -NotLike "$moduleRoot\tests\*" + $allFiles = Get-ChildItem -Path $moduleRoot -Recurse | Where-Object Name -like "*.ps1" | Where-Object FullName -NotLike "$moduleRoot\tests\*" foreach ($file in $allFiles) { $name = $file.FullName.Replace("$moduleRoot\", '') - It "[$name] Should have UTF8 encoding" { - Get-FileEncoding -Path $file.FullName | Should -Be 'UTF8' + It "[$name] Should have UTF8 encoding with Byte Order Mark" -TestCases @{ file = $file } { + Get-FileEncoding -Path $file.FullName | Should -Be 'UTF8 BOM' } - It "[$name] Should have no trailing space" { + It "[$name] Should have no trailing space" -TestCases @{ file = $file } { ($file | Select-String "\s$" | Where-Object { $_.Line.Trim().Length -gt 0}).LineNumber | Should -BeNullOrEmpty } @@ -51,38 +60,34 @@ Describe "Verifying integrity of module files" { $parseErrors = $null $ast = [System.Management.Automation.Language.Parser]::ParseFile($file.FullName, [ref]$tokens, [ref]$parseErrors) - It "[$name] Should have no syntax errors" { - $parseErrors | Should Be $Null + It "[$name] Should have no syntax errors" -TestCases @{ parseErrors = $parseErrors } { + $parseErrors | Should -BeNullOrEmpty } foreach ($command in $global:BannedCommands) { if ($global:MayContainCommand["$command"] -notcontains $file.Name) { - It "[$name] Should not use $command" { + It "[$name] Should not use $command" -TestCases @{ tokens = $tokens; command = $command } { $tokens | Where-Object Text -EQ $command | Should -BeNullOrEmpty } } } - - It "[$name] Should not contain aliases" { - $tokens | Where-Object TokenFlags -eq CommandName | Where-Object { Test-Path "alias:\$($_.Text)" } | Measure-Object | Select-Object -ExpandProperty Count | Should -Be 0 - } } } Context "Validating help.txt help files" { - $allFiles = Get-ChildItem -Path $moduleRoot -Recurse -Filter "*.help.txt" | Where-Object FullName -NotLike "$moduleRoot\tests\*" + $allFiles = Get-ChildItem -Path $moduleRoot -Recurse | Where-Object Name -like "*.help.txt" | Where-Object FullName -NotLike "$moduleRoot\tests\*" foreach ($file in $allFiles) { $name = $file.FullName.Replace("$moduleRoot\", '') - It "[$name] Should have UTF8 encoding" { - Get-FileEncoding -Path $file.FullName | Should -Be 'UTF8' + It "[$name] Should have UTF8 encoding" -TestCases @{ file = $file } { + Get-FileEncoding -Path $file.FullName | Should -Be 'UTF8 BOM' } - It "[$name] Should have no trailing space" { + It "[$name] Should have no trailing space" -TestCases @{ file = $file } { ($file | Select-String "\s$" | Where-Object { $_.Line.Trim().Length -gt 0 } | Measure-Object).Count | Should -Be 0 } } diff --git a/JEAnalyzer/tests/general/Help.Tests.ps1 b/JEAnalyzer/tests/general/Help.Tests.ps1 index 6d57594..c7c6da0 100644 --- a/JEAnalyzer/tests/general/Help.Tests.ps1 +++ b/JEAnalyzer/tests/general/Help.Tests.ps1 @@ -36,19 +36,21 @@ Param ( $SkipTest, [string[]] - $CommandPath = @("$PSScriptRoot\..\..\functions", "$PSScriptRoot\..\..\internal\functions"), + $CommandPath = @("$global:testroot\..\functions", "$global:testroot\..\internal\functions"), [string] $ModuleName = "JEAnalyzer", [string] - $ExceptionsFile = "$PSScriptRoot\Help.Exceptions.ps1" + $ExceptionsFile = "$global:testroot\general\Help.Exceptions.ps1" ) if ($SkipTest) { return } . $ExceptionsFile $includedNames = (Get-ChildItem $CommandPath -Recurse -File | Where-Object Name -like "*.ps1").BaseName -$commands = Get-Command -Module (Get-Module $ModuleName) -CommandType Cmdlet, Function, Workflow | Where-Object Name -in $includedNames +$commandTypes = @('Cmdlet', 'Function') +if ($PSVersionTable.PSEdition -eq 'Desktop' ) { $commandTypes += 'Workflow' } +$commands = Get-Command -Module (Get-Module $ModuleName) -CommandType $commandTypes | Where-Object Name -In $includedNames ## When testing help, remember that help is cached at the beginning of each session. ## To test, restart session. @@ -62,58 +64,32 @@ foreach ($command in $commands) { # The module-qualified command fails on Microsoft.PowerShell.Archive cmdlets $Help = Get-Help $commandName -ErrorAction SilentlyContinue - $testhelperrors = 0 - $testhelpall = 0 - Describe "Test help for $commandName" { - - $testhelpall += 1 - if ($Help.Synopsis -like '*`[``]*') { - # If help is not found, synopsis in auto-generated help is the syntax diagram - It "should not be auto-generated" { - $Help.Synopsis | Should -Not -BeLike '*`[``]*' - } - $testhelperrors += 1 - } - - $testhelpall += 1 - if ([String]::IsNullOrEmpty($Help.Description.Text)) { - # Should be a description for every function - It "gets description for $commandName" { - $Help.Description | Should -Not -BeNullOrEmpty - } - $testhelperrors += 1 - } + + Describe "Test help for $commandName" { - $testhelpall += 1 - if ([String]::IsNullOrEmpty(($Help.Examples.Example | Select-Object -First 1).Code)) { - # Should be at least one example - It "gets example code from $commandName" { - ($Help.Examples.Example | Select-Object -First 1).Code | Should -Not -BeNullOrEmpty - } - $testhelperrors += 1 - } + # If help is not found, synopsis in auto-generated help is the syntax diagram + It "should not be auto-generated" -TestCases @{ Help = $Help } { + $Help.Synopsis | Should -Not -BeLike '*`[``]*' + } - $testhelpall += 1 - if ([String]::IsNullOrEmpty(($Help.Examples.Example.Remarks | Select-Object -First 1).Text)) { - # Should be at least one example description - It "gets example help from $commandName" { - ($Help.Examples.Example.Remarks | Select-Object -First 1).Text | Should -Not -BeNullOrEmpty - } - $testhelperrors += 1 - } + # Should be a description for every function + It "gets description for $commandName" -TestCases @{ Help = $Help } { + $Help.Description | Should -Not -BeNullOrEmpty + } - if ($testhelperrors -eq 0) { - It "Ran silently $testhelpall tests" { - $testhelperrors | Should -be 0 - } - } + # Should be at least one example + It "gets example code from $commandName" -TestCases @{ Help = $Help } { + ($Help.Examples.Example | Select-Object -First 1).Code | Should -Not -BeNullOrEmpty + } + + # Should be at least one example description + It "gets example help from $commandName" -TestCases @{ Help = $Help } { + ($Help.Examples.Example.Remarks | Select-Object -First 1).Text | Should -Not -BeNullOrEmpty + } - $testparamsall = 0 - $testparamserrors = 0 Context "Test parameter help for $commandName" { - $Common = 'Debug', 'ErrorAction', 'ErrorVariable', 'InformationAction', 'InformationVariable', 'OutBuffer', 'OutVariable', - 'PipelineVariable', 'Verbose', 'WarningAction', 'WarningVariable' + $common = 'Debug', 'ErrorAction', 'ErrorVariable', 'InformationAction', 'InformationVariable', 'OutBuffer', 'OutVariable', 'PipelineVariable', 'Verbose', 'WarningAction', 'WarningVariable' $parameters = $command.ParameterSets.Parameters | Sort-Object -Property Name -Unique | Where-Object Name -notin $common $parameterNames = $parameters.Name @@ -121,79 +97,50 @@ foreach ($command in $commands) { foreach ($parameter in $parameters) { $parameterName = $parameter.Name $parameterHelp = $Help.parameters.parameter | Where-Object Name -EQ $parameterName + + # Should be a description for every parameter + It "gets help for parameter: $parameterName : in $commandName" -TestCases @{ parameterHelp = $parameterHelp } { + $parameterHelp.Description.Text | Should -Not -BeNullOrEmpty + } - $testparamsall += 1 - if ([String]::IsNullOrEmpty($parameterHelp.Description.Text)) { - # Should be a description for every parameter - It "gets help for parameter: $parameterName : in $commandName" { - $parameterHelp.Description.Text | Should -Not -BeNullOrEmpty - } - $testparamserrors += 1 - } - - $testparamsall += 1 $codeMandatory = $parameter.IsMandatory.toString() - if ($parameterHelp.Required -ne $codeMandatory) { - # Required value in Help should match IsMandatory property of parameter - It "help for $parameterName parameter in $commandName has correct Mandatory value" { - $parameterHelp.Required | Should -Be $codeMandatory - } - $testparamserrors += 1 - } + It "help for $parameterName parameter in $commandName has correct Mandatory value" -TestCases @{ parameterHelp = $parameterHelp; codeMandatory = $codeMandatory } { + $parameterHelp.Required | Should -Be $codeMandatory + } if ($HelpTestSkipParameterType[$commandName] -contains $parameterName) { continue } $codeType = $parameter.ParameterType.Name - $testparamsall += 1 if ($parameter.ParameterType.IsEnum) { # Enumerations often have issues with the typename not being reliably available $names = $parameter.ParameterType::GetNames($parameter.ParameterType) - if ($parameterHelp.parameterValueGroup.parameterValue -ne $names) { - # Parameter type in Help should match code - It "help for $commandName has correct parameter type for $parameterName" { - $parameterHelp.parameterValueGroup.parameterValue | Should -be $names - } - $testparamserrors += 1 - } + # Parameter type in Help should match code + It "help for $commandName has correct parameter type for $parameterName" -TestCases @{ parameterHelp = $parameterHelp; names = $names } { + $parameterHelp.parameterValueGroup.parameterValue | Should -be $names + } } elseif ($parameter.ParameterType.FullName -in $HelpTestEnumeratedArrays) { # Enumerations often have issues with the typename not being reliably available $names = [Enum]::GetNames($parameter.ParameterType.DeclaredMembers[0].ReturnType) - if ($parameterHelp.parameterValueGroup.parameterValue -ne $names) { - # Parameter type in Help should match code - It "help for $commandName has correct parameter type for $parameterName" { - $parameterHelp.parameterValueGroup.parameterValue | Should -be $names - } - $testparamserrors += 1 - } + It "help for $commandName has correct parameter type for $parameterName" -TestCases @{ parameterHelp = $parameterHelp; names = $names } { + $parameterHelp.parameterValueGroup.parameterValue | Should -be $names + } } else { # To avoid calling Trim method on a null object. $helpType = if ($parameterHelp.parameterValue) { $parameterHelp.parameterValue.Trim() } - if ($helpType -ne $codeType) { - # Parameter type in Help should match code - It "help for $commandName has correct parameter type for $parameterName" { - $helpType | Should -be $codeType - } - $testparamserrors += 1 - } + # Parameter type in Help should match code + It "help for $commandName has correct parameter type for $parameterName" -TestCases @{ helpType = $helpType; codeType = $codeType } { + $helpType | Should -be $codeType + } } } foreach ($helpParm in $HelpParameterNames) { - $testparamsall += 1 - if ($helpParm -notin $parameterNames) { - # Shouldn't find extra parameters in help. - It "finds help parameter in code: $helpParm" { - $helpParm -in $parameterNames | Should -Be $true - } - $testparamserrors += 1 - } - } - if ($testparamserrors -eq 0) { - It "Ran silently $testparamsall tests" { - $testparamserrors | Should -be 0 - } + # Shouldn't find extra parameters in help. + It "finds help parameter in code: $helpParm" -TestCases @{ helpParm = $helpParm; parameterNames = $parameterNames } { + $helpParm -in $parameterNames | Should -Be $true + } } } } diff --git a/JEAnalyzer/tests/general/PSScriptAnalyzer.Tests.ps1 b/JEAnalyzer/tests/general/PSScriptAnalyzer.Tests.ps1 index b9aba06..74e5a65 100644 --- a/JEAnalyzer/tests/general/PSScriptAnalyzer.Tests.ps1 +++ b/JEAnalyzer/tests/general/PSScriptAnalyzer.Tests.ps1 @@ -4,15 +4,15 @@ Param ( $SkipTest, [string[]] - $CommandPath = @("$PSScriptRoot\..\..\functions", "$PSScriptRoot\..\..\internal\functions") + $CommandPath = @("$global:testroot\..\functions", "$global:testroot\..\internal\functions") ) if ($SkipTest) { return } -$list = New-Object System.Collections.ArrayList +$global:__pester_data.ScriptAnalyzer = New-Object System.Collections.ArrayList Describe 'Invoking PSScriptAnalyzer against commandbase' { - $commandFiles = Get-ChildItem -Path $CommandPath -Recurse -Filter "*.ps1" + $commandFiles = Get-ChildItem -Path $CommandPath -Recurse | Where-Object Name -like "*.ps1" $scriptAnalyzerRules = Get-ScriptAnalyzerRule foreach ($file in $commandFiles) @@ -22,21 +22,19 @@ Describe 'Invoking PSScriptAnalyzer against commandbase' { forEach ($rule in $scriptAnalyzerRules) { - It "Should pass $rule" { + It "Should pass $rule" -TestCases @{ analysis = $analysis; rule = $rule } { If ($analysis.RuleName -contains $rule) { - $analysis | Where-Object RuleName -EQ $rule -outvariable failures | ForEach-Object { $list.Add($_) } + $analysis | Where-Object RuleName -EQ $rule -outvariable failures | ForEach-Object { $null = $global:__pester_data.ScriptAnalyzer.Add($_) } - 1 | Should Be 0 + 1 | Should -Be 0 } else { - 0 | Should Be 0 + 0 | Should -Be 0 } } } } } -} - -$list | Out-Default \ No newline at end of file +} \ No newline at end of file diff --git a/JEAnalyzer/tests/general/strings.Exceptions.ps1 b/JEAnalyzer/tests/general/strings.Exceptions.ps1 new file mode 100644 index 0000000..0a11c98 --- /dev/null +++ b/JEAnalyzer/tests/general/strings.Exceptions.ps1 @@ -0,0 +1,19 @@ +$exceptions = @{ } + +<# +A list of entries that MAY be in the language files, without causing the tests to fail. +This is commonly used in modules that generate localized messages straight from C#. +Specify the full key as it is written in the language files, do not prepend the modulename, +as you would have to in C# code. + +Example: +$exceptions['LegalSurplus'] = @( + 'Exception.Streams.FailedCreate' + 'Exception.Streams.FailedDispose' +) +#> +$exceptions['LegalSurplus'] = @( + +) + +$exceptions \ No newline at end of file diff --git a/JEAnalyzer/tests/general/strings.Tests.ps1 b/JEAnalyzer/tests/general/strings.Tests.ps1 new file mode 100644 index 0000000..a3c2e55 --- /dev/null +++ b/JEAnalyzer/tests/general/strings.Tests.ps1 @@ -0,0 +1,25 @@ +<# +.DESCRIPTION + This test verifies, that all strings that have been used, + are listed in the language files and thus have a message being displayed. + + It also checks, whether the language files have orphaned entries that need cleaning up. +#> + + + +Describe "Testing localization strings" { + $moduleRoot = (Get-Module JEAnalyzer).ModuleBase + $stringsResults = Export-PSMDString -ModuleRoot $moduleRoot + $exceptions = & "$global:testroot\general\strings.Exceptions.ps1" + + foreach ($stringEntry in $stringsResults) { + if ($stringEntry.String -eq "key") { continue } # Skipping the template default entry + It "Should be used & have text: $($stringEntry.String)" -TestCases @{ stringEntry = $stringEntry } { + if ($exceptions.LegalSurplus -notcontains $stringEntry.String) { + $stringEntry.Surplus | Should -BeFalse + } + $stringEntry.Text | Should -Not -BeNullOrEmpty + } + } +} \ No newline at end of file diff --git a/JEAnalyzer/tests/pester.ps1 b/JEAnalyzer/tests/pester.ps1 index d6600bc..6caf596 100644 --- a/JEAnalyzer/tests/pester.ps1 +++ b/JEAnalyzer/tests/pester.ps1 @@ -3,8 +3,9 @@ $TestFunctions = $true, - [ValidateSet('None', 'Default', 'Passed', 'Failed', 'Pending', 'Skipped', 'Inconclusive', 'Describe', 'Context', 'Summary', 'Header', 'Fails', 'All')] - $Show = "None", + [ValidateSet('None', 'Normal', 'Detailed', 'Diagnostic')] + [Alias('Show')] + $Output = "None", $Include = "*", @@ -15,10 +16,16 @@ Write-PSFMessage -Level Important -Message "Starting Tests" Write-PSFMessage -Level Important -Message "Importing Module" +$global:testroot = $PSScriptRoot +$global:__pester_data = @{ } + Remove-Module JEAnalyzer -ErrorAction Ignore Import-Module "$PSScriptRoot\..\JEAnalyzer.psd1" Import-Module "$PSScriptRoot\..\JEAnalyzer.psm1" -Force +# Need to import explicitly so we can use the configuration class +Import-Module Pester + Write-PSFMessage -Level Important -Message "Creating test result folder" $null = New-Item -Path "$PSScriptRoot\..\.." -Name TestResults -ItemType Directory -Force @@ -26,54 +33,69 @@ $totalFailed = 0 $totalRun = 0 $testresults = @() +$config = [PesterConfiguration]::Default +$config.TestResult.Enabled = $true #region Run General Tests -Write-PSFMessage -Level Important -Message "Modules imported, proceeding with general tests" -foreach ($file in (Get-ChildItem "$PSScriptRoot\general" -Filter "*.Tests.ps1")) +if ($TestGeneral) { - Write-PSFMessage -Level Significant -Message " Executing $($file.Name)" - $TestOuputFile = Join-Path "$PSScriptRoot\..\..\TestResults" "TEST-$($file.BaseName).xml" - $results = Invoke-Pester -Script $file.FullName -Show $Show -PassThru -OutputFile $TestOuputFile -OutputFormat NUnitXml - foreach ($result in $results) + Write-PSFMessage -Level Important -Message "Modules imported, proceeding with general tests" + foreach ($file in (Get-ChildItem "$PSScriptRoot\general" | Where-Object Name -like "*.Tests.ps1")) { - $totalRun += $result.TotalCount - $totalFailed += $result.FailedCount - $result.TestResult | Where-Object { -not $_.Passed } | ForEach-Object { - $name = $_.Name - $testresults += [pscustomobject]@{ - Describe = $_.Describe - Context = $_.Context - Name = "It $name" - Result = $_.Result - Message = $_.FailureMessage + if ($file.Name -notlike $Include) { continue } + if ($file.Name -like $Exclude) { continue } + + Write-PSFMessage -Level Significant -Message " Executing $($file.Name)" + $config.TestResult.OutputPath = Join-Path "$PSScriptRoot\..\..\TestResults" "TEST-$($file.BaseName).xml" + $config.Run.Path = $file.FullName + $config.Run.PassThru = $true + $config.Output.Verbosity = $Output + $results = Invoke-Pester -Configuration $config + foreach ($result in $results) + { + $totalRun += $result.TotalCount + $totalFailed += $result.FailedCount + $result.Tests | Where-Object Result -ne 'Passed' | ForEach-Object { + $testresults += [pscustomobject]@{ + Block = $_.Block + Name = "It $($_.Name)" + Result = $_.Result + Message = $_.ErrorRecord.DisplayErrorMessage + } } } } } #endregion Run General Tests +$global:__pester_data.ScriptAnalyzer | Out-Host + #region Test Commands -Write-PSFMessage -Level Important -Message "Proceeding with individual tests" -foreach ($file in (Get-ChildItem "$PSScriptRoot\functions" -Recurse -File -Filter "*Tests.ps1")) +if ($TestFunctions) { - if ($file.Name -notlike $Include) { continue } - if ($file.Name -like $Exclude) { continue } - - Write-PSFMessage -Level Significant -Message " Executing $($file.Name)" - $TestOuputFile = Join-Path "$PSScriptRoot\..\..\TestResults" "TEST-$($file.BaseName).xml" - $results = Invoke-Pester -Script $file.FullName -Show $Show -PassThru -OutputFile $TestOuputFile -OutputFormat NUnitXml - foreach ($result in $results) + Write-PSFMessage -Level Important -Message "Proceeding with individual tests" + foreach ($file in (Get-ChildItem "$PSScriptRoot\functions" -Recurse -File | Where-Object Name -like "*Tests.ps1")) { - $totalRun += $result.TotalCount - $totalFailed += $result.FailedCount - $result.TestResult | Where-Object { -not $_.Passed } | ForEach-Object { - $name = $_.Name - $testresults += [pscustomobject]@{ - Describe = $_.Describe - Context = $_.Context - Name = "It $name" - Result = $_.Result - Message = $_.FailureMessage + if ($file.Name -notlike $Include) { continue } + if ($file.Name -like $Exclude) { continue } + + Write-PSFMessage -Level Significant -Message " Executing $($file.Name)" + $config.TestResult.OutputPath = Join-Path "$PSScriptRoot\..\..\TestResults" "TEST-$($file.BaseName).xml" + $config.Run.Path = $file.FullName + $config.Run.PassThru = $true + $config.Output.Verbosity = $Output + $results = Invoke-Pester -Configuration $config + foreach ($result in $results) + { + $totalRun += $result.TotalCount + $totalFailed += $result.FailedCount + $result.Tests | Where-Object Result -ne 'Passed' | ForEach-Object { + $testresults += [pscustomobject]@{ + Block = $_.Block + Name = "It $($_.Name)" + Result = $_.Result + Message = $_.ErrorRecord.DisplayErrorMessage + } } } } From 0d428d43c52a174b4d15ce7c27013552bc9e9706 Mon Sep 17 00:00:00 2001 From: Friedrich Weinmann Date: Fri, 2 Oct 2020 09:24:05 +0200 Subject: [PATCH 3/3] fixing tests --- .../construct/Import-JeaScriptFile.ps1 | 8 ++--- .../functions/write/Install-JeaModule.ps1 | 6 ++++ JEAnalyzer/tests/general/Manifest.Tests.ps1 | 34 +++++++++++++------ 3 files changed, 32 insertions(+), 16 deletions(-) diff --git a/JEAnalyzer/functions/construct/Import-JeaScriptFile.ps1 b/JEAnalyzer/functions/construct/Import-JeaScriptFile.ps1 index c1ae86a..7b483f8 100644 --- a/JEAnalyzer/functions/construct/Import-JeaScriptFile.ps1 +++ b/JEAnalyzer/functions/construct/Import-JeaScriptFile.ps1 @@ -145,7 +145,7 @@ function {0} #region Case: Parse Error elseif ($testResult.ErrorType -eq 'ParseError') { - Stop-PSFFunction -String 'Import-JeaScriptFile.ParsingError' -StringValues $file -Continue + Stop-PSFFunction -String 'Import-JeaScriptFile.ParsingError' -StringValues $file -Continue -EnableException $EnableException } #endregion Case: Parse Error @@ -165,13 +165,9 @@ function {0} #region Case: Unknown State (Should never happen) else { - Stop-PSFFunction -String 'Import-JeaScriptFile.UnknownError' -StringValues $file -Continue + Stop-PSFFunction -String 'Import-JeaScriptFile.UnknownError' -StringValues $file -Continue -EnableException $EnableException } #endregion Case: Unknown State (Should never happen) } } - end - { - - } } \ No newline at end of file diff --git a/JEAnalyzer/functions/write/Install-JeaModule.ps1 b/JEAnalyzer/functions/write/Install-JeaModule.ps1 index 136666f..5c8f782 100644 --- a/JEAnalyzer/functions/write/Install-JeaModule.ps1 +++ b/JEAnalyzer/functions/write/Install-JeaModule.ps1 @@ -26,6 +26,12 @@ 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 diff --git a/JEAnalyzer/tests/general/Manifest.Tests.ps1 b/JEAnalyzer/tests/general/Manifest.Tests.ps1 index c442222..5fe5b6c 100644 --- a/JEAnalyzer/tests/general/Manifest.Tests.ps1 +++ b/JEAnalyzer/tests/general/Manifest.Tests.ps1 @@ -1,47 +1,61 @@ Describe "Validating the module manifest" { - $moduleRoot = (Resolve-Path "$PSScriptRoot\..\..").Path + $moduleRoot = (Resolve-Path "$global:testroot\..").Path $manifest = ((Get-Content "$moduleRoot\JEAnalyzer.psd1") -join "`n") | Invoke-Expression Context "Basic resources validation" { - $files = Get-ChildItem "$moduleRoot\functions" -Recurse -File -Filter "*.ps1" - It "Exports all functions in the public folder" { + $files = Get-ChildItem "$moduleRoot\functions" -Recurse -File | Where-Object Name -like "*.ps1" + It "Exports all functions in the public folder" -TestCases @{ files = $files; manifest = $manifest } { $functions = (Compare-Object -ReferenceObject $files.BaseName -DifferenceObject $manifest.FunctionsToExport | Where-Object SideIndicator -Like '<=').InputObject $functions | Should -BeNullOrEmpty } - It "Exports no function that isn't also present in the public folder" { + It "Exports no function that isn't also present in the public folder" -TestCases @{ files = $files; manifest = $manifest } { $functions = (Compare-Object -ReferenceObject $files.BaseName -DifferenceObject $manifest.FunctionsToExport | Where-Object SideIndicator -Like '=>').InputObject $functions | Should -BeNullOrEmpty } - It "Exports none of its internal functions" { + It "Exports none of its internal functions" -TestCases @{ moduleRoot = $moduleRoot; manifest = $manifest } { $files = Get-ChildItem "$moduleRoot\internal\functions" -Recurse -File -Filter "*.ps1" $files | Where-Object BaseName -In $manifest.FunctionsToExport | Should -BeNullOrEmpty } } Context "Individual file validation" { - It "The root module file exists" { + It "The root module file exists" -TestCases @{ moduleRoot = $moduleRoot; manifest = $manifest } { Test-Path "$moduleRoot\$($manifest.RootModule)" | Should -Be $true } foreach ($format in $manifest.FormatsToProcess) { - It "The file $format should exist" { + It "The file $format should exist" -TestCases @{ moduleRoot = $moduleRoot; format = $format } { Test-Path "$moduleRoot\$format" | Should -Be $true } } foreach ($type in $manifest.TypesToProcess) { - It "The file $type should exist" { + It "The file $type should exist" -TestCases @{ moduleRoot = $moduleRoot; type = $type } { Test-Path "$moduleRoot\$type" | Should -Be $true } } foreach ($assembly in $manifest.RequiredAssemblies) { - It "The file $assembly should exist" { - Test-Path "$moduleRoot\$assembly" | Should -Be $true + if ($assembly -like "*.dll") { + It "The file $assembly should exist" -TestCases @{ moduleRoot = $moduleRoot; assembly = $assembly } { + Test-Path "$moduleRoot\$assembly" | Should -Be $true + } + } + else { + It "The file $assembly should load from the GAC" -TestCases @{ moduleRoot = $moduleRoot; assembly = $assembly } { + { Add-Type -AssemblyName $assembly } | Should -Not -Throw + } + } + } + + foreach ($tag in $manifest.PrivateData.PSData.Tags) + { + It "Tags should have no spaces in name" -TestCases @{ tag = $tag } { + $tag -match " " | Should -Be $false } } }