Skip to content
6 changes: 6 additions & 0 deletions PSDepend/PSDependMap.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,10 @@
Description = 'Support dependencies by handling simple tasks'
Supports = 'windows', 'core', 'macos', 'linux'
}

WindowsRSAT = @{
Script = 'WindowsRSAT.ps1'
Description = 'Install a WindowsRSAT PowerShell module using Add-WindowsCapability or Install-WindowsFeature, depending on OS'
Supports = 'windows'
}
}
164 changes: 164 additions & 0 deletions PSDepend/PSDependScripts/WindowsRSAT.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
<#
.SYNOPSIS
'Install a WindowsRSAT PowerShell module using Add-WindowsCapability or Install-WindowsFeature, depending on OS'

.DESCRIPTION
Installs a RSAT Module in Windows.

Relevant Dependency metadata:
Name: The name for the module to install

.PARAMETER PSDependAction
Test, Install, or Import the module. Defaults to Install

Test: Return true or false on whether the dependency is in place
Install: Install the dependency
Import: Import the dependency

.EXAMPLE
@{
ActiveDirectory = @{
DependencyType = 'WindowsRSAT'
Name = 'ActiveDirectory'
}
}
#>
[cmdletbinding()]
param(
[PSTypeName('PSDepend.Dependency')]
[psobject[]]$Dependency,

[ValidateSet('Test', 'Install', 'Import')]
[string[]]$PSDependAction = @('Install')
)


$RSAT_MODULE_MAP = @{
'ActiveDirectory' = @{
'WindowsFeature' = 'RSAT-AD-Powershell'
'WindowsCapability' = 'Rsat.ActiveDirectory.DS-LDS.Tools'
}
'ADDSDeployment' = @{
'WindowsFeature' = 'RSAT-AD-Powershell'
'WindowsCapability' = 'Rsat.ActiveDirectory.DS-LDS.Tools'
}
'ADCSAdministration' = @{
'WindowsFeature' = 'RSAT-ADCS-Mgmt'
'WindowsCapability' = 'Rsat.CertificateServices.Tools'
}
'ADCSDeployment' = @{
'WindowsFeature' = 'RSAT-ADCS-Mgmt'
'WindowsCapability' = 'Rsat.CertificateServices.Tools'
}
'ADRMS' = @{
'WindowsFeature' = 'RSAT-ADRMS'
#'WindowsCapability' = 'Rsat.CertificateServices.Tools'
}
'ADRMSAdmin' = @{
'WindowsFeature' = 'RSAT-ADRMS'
#'WindowsCapability' = 'Rsat.CertificateServices.Tools'
}
'BitLocker' = @{
'WindowsFeature' = 'RSAT-Feature-Tools-BitLocker-RemoteAdminTool'
'WindowsCapability' = 'Rsat.BitLocker.Recovery.Tools'
}
'DFSN' = @{
'WindowsFeature' = 'RSAT-DFS-Mgmt-Con'
#'WindowsCapability' = 'Rsat.BitLocker.Recovery.Tools'
}
'DFSR' = @{
'WindowsFeature' = 'RSAT-DFS-Mgmt-Con'
#'WindowsCapability' = 'Rsat.BitLocker.Recovery.Tools'
}
'DHCP' = @{
'WindowsFeature' = 'RSAT-DHCP'
'WindowsCapability' = 'Rsat.DHCP.Tools'
}
'DNSClient' = @{
'WindowsFeature' = 'RSAT-DNS-Server'
'WindowsCapability' = 'rsat.dns.tools'
}
'DNSServer' = @{
'WindowsFeature' = 'RSAT-DNS-Server'
'WindowsCapability' = 'rsat.dns.tools'
}
'FailoverClusters' = @{
'WindowsFeature' = 'RSAT-Clustering-PowerShell'
'WindowsCapability' = 'Rsat.FailoverCluster.Management.Tools'
}
'FileServerResourceManager' = @{
'WindowsFeature' = 'RSAT-FSRM-Mgmt'
#'WindowsCapability' = 'Rsat.FileServices.Tools'
}
'GroupPolicy' = @{
'WindowsFeature' = 'GPMC'
'WindowsCapability' = 'Rsat.GroupPolicy.Management.Tools'
}
'Hyper-V' = @{
'WindowsFeature' = 'RSAT-Hyper-V-Tools'
#'WindowsCapability' = 'Rsat.GroupPolicy.Management.Tools'
}
'IISAdministration' = @{
'WindowsFeature' = 'web-mgmt-console'
#'WindowsCapability' = 'Rsat.GroupPolicy.Management.Tools'
}
'RemoteAccess' = @{
'WindowsFeature' = 'RSAT-RemoteAccess-Powershell'
'WindowsCapability' = 'Rsat.RemoteAccess.Management.Tools'
}
'VAMT' = @{
'WindowsFeature' = 'RSAT-VA-Tools'
'WindowsCapability' = 'Rsat.VolumeActivation.Tools'
}
}

# Extract data from Dependency
$ModuleName = $Dependency.Name
if (-not $ModuleName) {
$ModuleName = $Dependency.DependencyName
}

if (Get-Module -ListAvailable -Name $ModuleName -ErrorAction SilentlyContinue) {
Write-Verbose "Found existing module [$ModuleName]"
if ($PSDependAction -contains 'Test') {
return $True
}
return $null
}

#No dependency found, return false if we're testing alone...
if ( $PSDependAction -contains 'Test' -and $PSDependAction.count -eq 1) {
return $False
}

if ($PSDependAction -contains 'Install') {

if (-not (Test-Administrator)) {
throw "Must be an admin to install RSAT modules"
}

#Server
$Type = 'WindowsFeature'
if ((get-CimInstance -ClassName Win32_OperatingSystem).ProductType -eq 1) {
# Workstation
$Type = 'WindowsCapability'
}

if (-not $RSAT_MODULE_MAP.ContainsKey($ModuleName)) {
throw "Unknown Module $ModuleName"
}

if ($null -eq $RSAT_MODULE_MAP[$ModuleName][$type]) {
throw "Unknown Module $ModuleName"
}

if ($Type -eq 'WindowsFeature') {
$null = install-windowsfeature -name $RSAT_MODULE_MAP[$ModuleName][$Type]
}
else {
$null = Add-WindowsCapability -Online -Name $RSAT_MODULE_MAP[$ModuleName][$Type]
}
}

# Conditional import
Import-PSDependModule -Name $ModuleName -Action $PSDependAction
9 changes: 9 additions & 0 deletions PSDepend/Private/Test-Administrator.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
function Test-Administrator {
[CmdletBinding()]
[OutputType([bool])]
param()

([Security.Principal.WindowsPrincipal]::new(
[Security.Principal.WindowsIdentity]::GetCurrent()
)).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}
156 changes: 156 additions & 0 deletions Tests/WindowsRSAT.Type.Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
#requires -Module @{ ModuleName = 'Pester'; ModuleVersion = '5.0.0' }

BeforeDiscovery {
Import-Module (Join-Path $PSScriptRoot 'Shared/TestHelpers.psm1') -Force
$script:SkipUnsupported = -not (Test-PSDependTypeSupportedHere -DependencyType 'WindowsRSAT')
Comment on lines +4 to +5
}
Comment on lines +1 to +6

BeforeAll {
if (-not $env:BHProjectPath) {
& "$PSScriptRoot\..\build.ps1" -Task 'Build'
}
Comment on lines +10 to +11
Remove-Module $env:BHProjectName -ErrorAction SilentlyContinue
Import-Module (Join-Path $env:BHProjectPath $env:BHProjectName) -Force

Import-Module (Join-Path $PSScriptRoot 'Shared/TestHelpers.psm1') -Force

$script:ScriptPath = Join-Path $env:BHProjectPath 'PSDepend/PSDependScripts/WindowsRSAT.ps1'

# Inject stub functions for the install-side cmdlets into the PSDepend
# module scope so Pester's Mock attaches to a regular PowerShell function
# rather than to the underlying CDXML/binary cmdlets. PowerShell resolves
# functions before cmdlets in the same scope, so on hosts where the real
# cmdlets exist (Windows Server PS 5.1 with ServerManager, Windows client
# with DISM) the stub still wins. Mocking the real CDXML cmdlets has been
# observed to silently not intercept on Windows PowerShell 5.1 -- the
# stub-function approach makes mocking work consistently across PS 5.1,
# PS 7, and all platforms.
InModuleScope PSDepend {
function script:Install-WindowsFeature { [CmdletBinding()] param([string]$Name) }
function script:Add-WindowsCapability { [CmdletBinding()] param([switch]$Online, [string]$Name) }
}
}

Describe 'WindowsRSAT script' -Tag 'WindowsOnly' -Skip:$SkipUnsupported {

BeforeAll {
InModuleScope PSDepend {
Mock Get-Module { } -ParameterFilter { $ListAvailable }
Mock Install-WindowsFeature { }
Mock Add-WindowsCapability { }
Mock Get-CimInstance { [PSCustomObject]@{ ProductType = 3 } } -ParameterFilter { $ClassName -eq 'Win32_OperatingSystem' }
Mock Import-PSDependModule { }
Mock Test-Administrator { $true }
}
}

Context 'PSDependAction = Test only' {
It 'Returns $false when the module is not installed' {
$dep = New-PSDependFixture -DependencyName 'ActiveDirectory' -DependencyType 'WindowsRSAT'
$result = InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } {
& $ScriptPath -Dependency $Dep -PSDependAction Test
}
$result | Should -Be $false
Should -Invoke -CommandName Install-WindowsFeature -ModuleName PSDepend -Times 0
Should -Invoke -CommandName Add-WindowsCapability -ModuleName PSDepend -Times 0
}

It 'Returns $true when the module is already available' {
InModuleScope PSDepend {
Mock Get-Module { [PSCustomObject]@{ Name = 'ActiveDirectory' } } -ParameterFilter { $ListAvailable }
}
$dep = New-PSDependFixture -DependencyName 'ActiveDirectory' -DependencyType 'WindowsRSAT'
$result = InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } {
& $ScriptPath -Dependency $Dep -PSDependAction Test
}
$result | Should -Be $true
Should -Invoke -CommandName Install-WindowsFeature -ModuleName PSDepend -Times 0
}
}

Context 'PSDependAction = Install on Server' {

It 'Dispatches to Install-WindowsFeature with the mapped name (<ModuleName> -> <Feature>)' -TestCases @(
@{ ModuleName = 'ActiveDirectory'; Feature = 'RSAT-AD-Powershell' }
@{ ModuleName = 'BitLocker'; Feature = 'RSAT-Feature-Tools-BitLocker-RemoteAdminTool' }
@{ ModuleName = 'Hyper-V'; Feature = 'RSAT-Hyper-V-Tools' }
@{ ModuleName = 'GroupPolicy'; Feature = 'GPMC' }
) {
param($ModuleName, $Feature)

$dep = New-PSDependFixture -DependencyName $ModuleName -DependencyType 'WindowsRSAT'
InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } {
& $ScriptPath -Dependency $Dep -PSDependAction Install
}
Should -Invoke -CommandName Install-WindowsFeature -ModuleName PSDepend -Times 1 -Exactly -ParameterFilter {
$Name -eq $Feature
}
Should -Invoke -CommandName Add-WindowsCapability -ModuleName PSDepend -Times 0
}

It 'Throws when the module name is not in the mapping table' {
$dep = New-PSDependFixture -DependencyName 'NotARealModule' -DependencyType 'WindowsRSAT'
{
InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } {
& $ScriptPath -Dependency $Dep -PSDependAction Install
}
} | Should -Throw '*Unknown Module*'
}
}

Context 'PSDependAction = Install on Workstation' {

BeforeAll {
InModuleScope PSDepend {
Mock Get-CimInstance { [PSCustomObject]@{ ProductType = 1 } } -ParameterFilter { $ClassName -eq 'Win32_OperatingSystem' }
}
}

It 'Dispatches to Add-WindowsCapability with the mapped name (<ModuleName> -> <Capability>)' -TestCases @(
@{ ModuleName = 'ActiveDirectory'; Capability = 'Rsat.ActiveDirectory.DS-LDS.Tools' }
@{ ModuleName = 'BitLocker'; Capability = 'Rsat.BitLocker.Recovery.Tools' }
@{ ModuleName = 'GroupPolicy'; Capability = 'Rsat.GroupPolicy.Management.Tools' }
) {
param($ModuleName, $Capability)

$dep = New-PSDependFixture -DependencyName $ModuleName -DependencyType 'WindowsRSAT'
InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } {
& $ScriptPath -Dependency $Dep -PSDependAction Install
}
Should -Invoke -CommandName Add-WindowsCapability -ModuleName PSDepend -Times 1 -Exactly -ParameterFilter {
$Name -eq $Capability
}
Should -Invoke -CommandName Install-WindowsFeature -ModuleName PSDepend -Times 0
}
}

Context 'PSDependAction = Install gated by admin check' {
It 'Throws when Test-Administrator returns $false' {
InModuleScope PSDepend {
Mock Test-Administrator { $false }
}
$dep = New-PSDependFixture -DependencyName 'ActiveDirectory' -DependencyType 'WindowsRSAT'
{
InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } {
& $ScriptPath -Dependency $Dep -PSDependAction Install
}
} | Should -Throw '*admin*'
Should -Invoke -CommandName Install-WindowsFeature -ModuleName PSDepend -Times 0
Should -Invoke -CommandName Add-WindowsCapability -ModuleName PSDepend -Times 0
}
}

Context 'PSDependAction = Test, Install short-circuits when installed' {
It 'Skips Install when the module is already available' {
InModuleScope PSDepend {
Mock Get-Module { [PSCustomObject]@{ Name = 'ActiveDirectory' } } -ParameterFilter { $ListAvailable }
}
$dep = New-PSDependFixture -DependencyName 'ActiveDirectory' -DependencyType 'WindowsRSAT'
InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } {
& $ScriptPath -Dependency $Dep -PSDependAction Test, Install
}
Should -Invoke -CommandName Install-WindowsFeature -ModuleName PSDepend -Times 0
Should -Invoke -CommandName Add-WindowsCapability -ModuleName PSDepend -Times 0
}
}
}
Loading