diff --git a/build.psm1 b/build.psm1 index 1b9cacfdb5..0c2d84b011 100644 --- a/build.psm1 +++ b/build.psm1 @@ -538,8 +538,9 @@ Fix steps: # publish powershell.config.json $config = @{} - if ($Environment.IsWindows) { - $config = @{ "Microsoft.PowerShell:ExecutionPolicy" = "RemoteSigned" } + if ($environment.IsWindows) { + $config = @{ "Microsoft.PowerShell:ExecutionPolicy" = "RemoteSigned"; + "WindowsPowerShellCompatibilityModuleDenyList" = @("PSScheduledJob","BestPractices","UpdateServices") } } # When building preview, we want the configuration to enable all experiemental features by default diff --git a/src/System.Management.Automation/engine/Modules/ImportModuleCommand.cs b/src/System.Management.Automation/engine/Modules/ImportModuleCommand.cs index c55bbfba13..745c69bbf6 100644 --- a/src/System.Management.Automation/engine/Modules/ImportModuleCommand.cs +++ b/src/System.Management.Automation/engine/Modules/ImportModuleCommand.cs @@ -4,11 +4,13 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Linq; using System.Management.Automation; +using System.Management.Automation.Configuration; using System.Management.Automation.Internal; using System.Management.Automation.Language; using System.Management.Automation.Runspaces; @@ -1878,10 +1880,81 @@ namespace Microsoft.PowerShell.Commands } } + private bool IsModuleInDenyList(string[] moduleDenyList, string moduleName, ModuleSpecification moduleSpec) + { + Debug.Assert(string.IsNullOrEmpty(moduleName) ^ (moduleSpec == null), "Either moduleName or moduleSpec must be specified"); + + var exactModuleName = string.Empty; + bool match = false; + + if (!string.IsNullOrEmpty(moduleName)) + { + // moduleName can be just a module name and it also can be a full path to psd1 from which we need to extract the module name + exactModuleName = Path.GetFileNameWithoutExtension(moduleName); + } + else if (moduleSpec != null) + { + exactModuleName = moduleSpec.Name; + } + + foreach (var deniedModuleName in moduleDenyList) + { + // use case-insensitive module name comparison + match = exactModuleName.Equals(deniedModuleName, StringComparison.InvariantCultureIgnoreCase); + if (match) + { + string errorMessage = string.Format(CultureInfo.InvariantCulture, Modules.WinCompatModuleInDenyList, exactModuleName); + InvalidOperationException exception = new InvalidOperationException(errorMessage); + ErrorRecord er = new ErrorRecord(exception, "Modules_ModuleInWinCompatDenyList", ErrorCategory.ResourceUnavailable, exactModuleName); + WriteError(er); + break; + } + } + + return match; + } + + private List FilterModuleCollection(IEnumerable moduleCollection) + { + List filteredModuleCollection = null; + if (moduleCollection != null) + { + // the ModuleDeny list is cached in PowerShellConfig object + string[] moduleDenyList = PowerShellConfig.Instance.GetWindowsPowerShellCompatibilityModuleDenyList(); + if (moduleDenyList?.Any() != true) + { + filteredModuleCollection = new List(moduleCollection); + } + else + { + filteredModuleCollection = new List(); + foreach (var module in moduleCollection) + { + if (!IsModuleInDenyList(moduleDenyList, module as string, module as ModuleSpecification)) + { + filteredModuleCollection.Add(module); + } + } + } + } + + return filteredModuleCollection; + } + internal override IList ImportModulesUsingWinCompat(IEnumerable moduleNames, IEnumerable moduleFullyQualifiedNames, ImportModuleOptions importModuleOptions) { IList moduleProxyList = new List(); #if !UNIX + // one of the two parameters can be passed: either ModuleNames (most of the time) or ModuleSpecifications (they are used in different parameter sets) + List filteredModuleNames = FilterModuleCollection(moduleNames); + List filteredModuleFullyQualifiedNames = FilterModuleCollection(moduleFullyQualifiedNames); + + // do not setup WinCompat resources if we have no modules to import + if ((filteredModuleNames?.Any() != true) && (filteredModuleFullyQualifiedNames?.Any() != true)) + { + return moduleProxyList; + } + var winPSVersionString = Utils.GetWindowsPowerShellVersionFromRegistry(); if (!winPSVersionString.StartsWith("5.1", StringComparison.OrdinalIgnoreCase)) { @@ -1895,8 +1968,9 @@ namespace Microsoft.PowerShell.Commands return new List(); } - moduleProxyList = ImportModule_RemotelyViaPsrpSession(importModuleOptions, moduleNames, moduleFullyQualifiedNames, WindowsPowerShellCompatRemotingSession, usingWinCompat: true); - foreach(PSModuleInfo moduleProxy in moduleProxyList) + moduleProxyList = ImportModule_RemotelyViaPsrpSession(importModuleOptions, filteredModuleNames, filteredModuleFullyQualifiedNames, WindowsPowerShellCompatRemotingSession, usingWinCompat: true); + + foreach (PSModuleInfo moduleProxy in moduleProxyList) { moduleProxy.IsWindowsPowerShellCompatModule = true; System.Threading.Interlocked.Increment(ref s_WindowsPowerShellCompatUsageCounter); diff --git a/src/System.Management.Automation/engine/PSConfiguration.cs b/src/System.Management.Automation/engine/PSConfiguration.cs index 1b97781c73..c25eb20e1b 100644 --- a/src/System.Management.Automation/engine/PSConfiguration.cs +++ b/src/System.Management.Automation/engine/PSConfiguration.cs @@ -50,6 +50,7 @@ namespace System.Management.Automation.Configuration private const string ConfigFileName = "powershell.config.json"; private const string ExecutionPolicyDefaultShellKey = "Microsoft.PowerShell:ExecutionPolicy"; private const string DisableImplicitWinCompatKey = "DisableImplicitWinCompat"; + private const string WindowsPowerShellCompatibilityModuleDenyListKey = "WindowsPowerShellCompatibilityModuleDenyList"; // Provide a singleton internal static readonly PowerShellConfig Instance = new PowerShellConfig(); @@ -227,6 +228,18 @@ namespace System.Management.Automation.Configuration return !settingValue.Value; } + internal string[] GetWindowsPowerShellCompatibilityModuleDenyList() + { + string[] settingValue = ReadValueFromFile(ConfigScope.CurrentUser, WindowsPowerShellCompatibilityModuleDenyListKey); + if (settingValue == null) + { + // if the setting is not mentioned in configuration files, then the default WindowsPowerShellCompatibilityModuleDenyList value is null + settingValue = ReadValueFromFile(ConfigScope.AllUsers, WindowsPowerShellCompatibilityModuleDenyListKey); + } + + return settingValue; + } + /// /// Corresponding settings of the original Group Policies. /// diff --git a/src/System.Management.Automation/resources/Modules.resx b/src/System.Management.Automation/resources/Modules.resx index f37f165c68..a74152b971 100644 --- a/src/System.Management.Automation/resources/Modules.resx +++ b/src/System.Management.Automation/resources/Modules.resx @@ -516,6 +516,9 @@ Detected Windows PowerShell version {0}. Windows PowerShell 5.1 is required to load modules using Windows PowerShell compatibility feature. Install Windows Management Framework (WMF) 5.1 from https://aka.ms/WMF5Download to enable this feature. + + Module '{0}' is blocked from loading using Windows PowerShell compatibility feature by a 'WindowsPowerShellCompatibilityModuleDenyList' setting in PowerShell configuration file. + The module {0} cannot be imported over a CimSession. Try using the PSSession parameter of the Import-Module cmdlet. diff --git a/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Module.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Module.Tests.ps1 index 7a3ed362b3..250fd89738 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Module.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Module.Tests.ps1 @@ -450,26 +450,66 @@ Describe "Additional tests for Import-Module with WinCompat" -Tag "Feature" { $LogPath = Join-Path $TestDrive (New-Guid).ToString() $ConfigPath = Join-Path $TestDrive 'powershell.config.json' '{"DisableImplicitWinCompat" : "True"}' | Out-File -Force $ConfigPath - pwsh -NoProfile -NonInteractive -settingsFile $ConfigPath -c "[System.Management.Automation.Internal.InternalTestHooks]::SetTestHook('TestWindowsPowerShellPSHomeLocation', `'$basePath`');Import-Module $ModuleName2" *> $LogPath + & $pwsh -NoProfile -NonInteractive -settingsFile $ConfigPath -c "[System.Management.Automation.Internal.InternalTestHooks]::SetTestHook('TestWindowsPowerShellPSHomeLocation', `'$basePath`');Import-Module $ModuleName2" *> $LogPath $LogPath | Should -FileContentMatch 'cannot be loaded implicitly using the Windows Compatibility' } It "Fails to auto-import incompatible module during CommandDiscovery\ModuleAutoload if implicit WinCompat is Disabled in config" { $LogPath = Join-Path $TestDrive (New-Guid).ToString() $ConfigPath = Join-Path $TestDrive 'powershell.config.json' - '{"DisableImplicitWinCompat" : "True"}' | Out-File -Force $ConfigPath - pwsh -NoProfile -NonInteractive -settingsFile $ConfigPath -c "[System.Management.Automation.Internal.InternalTestHooks]::SetTestHook('TestWindowsPowerShellPSHomeLocation', `'$basePath`'); Test-$ModuleName2" *> $LogPath + '{"DisableImplicitWinCompat" : "True","Microsoft.PowerShell:ExecutionPolicy": "RemoteSigned"}' | Out-File -Force $ConfigPath + & $pwsh -NoProfile -NonInteractive -settingsFile $ConfigPath -c "[System.Management.Automation.Internal.InternalTestHooks]::SetTestHook('TestWindowsPowerShellPSHomeLocation', `'$basePath`'); Test-$ModuleName2" *> $LogPath $LogPath | Should -FileContentMatch 'not recognized as the name of a cmdlet' } It "Successfully auto-imports incompatible module during CommandDiscovery\ModuleAutoload if implicit WinCompat is Enabled in config" { $LogPath = Join-Path $TestDrive (New-Guid).ToString() $ConfigPath = Join-Path $TestDrive 'powershell.config.json' - '{"DisableImplicitWinCompat" : "False"}' | Out-File -Force $ConfigPath - pwsh -NoProfile -NonInteractive -settingsFile $ConfigPath -c "[System.Management.Automation.Internal.InternalTestHooks]::SetTestHook('TestWindowsPowerShellPSHomeLocation', `'$basePath`'); Test-$ModuleName2" *> $LogPath + '{"DisableImplicitWinCompat" : "False","Microsoft.PowerShell:ExecutionPolicy": "RemoteSigned"}' | Out-File -Force $ConfigPath + & $pwsh -NoProfile -NonInteractive -settingsFile $ConfigPath -c "[System.Management.Automation.Internal.InternalTestHooks]::SetTestHook('TestWindowsPowerShellPSHomeLocation', `'$basePath`'); Test-$ModuleName2" *> $LogPath $LogPath | Should -FileContentMatch 'True' } } + + Context "Tests around Windows PowerShell Compatibility module deny list" { + BeforeAll { + $pwsh = "$PSHOME/pwsh" + Add-ModulePath $basePath + $ConfigPath = Join-Path $TestDrive 'powershell.config.json' + } + + AfterAll { + Restore-ModulePath + } + + It "Successfully imports incompatible module when DenyList is not specified in powershell.config.json" { + '{"Microsoft.PowerShell:ExecutionPolicy": "RemoteSigned"}' | Out-File -Force $ConfigPath + & $pwsh -NoProfile -NonInteractive -settingsFile $ConfigPath -c "[System.Management.Automation.Internal.InternalTestHooks]::SetTestHook('TestWindowsPowerShellPSHomeLocation', `'$basePath`');Import-Module $ModuleName2 -WarningAction Ignore;Test-${ModuleName2}PSEdition" | Should -Be 'Desktop' + } + + It "Successfully imports incompatible module when DenyList is empty" { + '{"Microsoft.PowerShell:ExecutionPolicy": "RemoteSigned","WindowsPowerShellCompatibilityModuleDenyList": []}' | Out-File -Force $ConfigPath + & $pwsh -NoProfile -NonInteractive -settingsFile $ConfigPath -c "[System.Management.Automation.Internal.InternalTestHooks]::SetTestHook('TestWindowsPowerShellPSHomeLocation', `'$basePath`');Import-Module $ModuleName2 -WarningAction Ignore;Test-${ModuleName2}PSEdition" | Should -Be 'Desktop' + } + + It "Blocks DenyList module import by Import-Module -UseWindowsPowerShell" { + '{"WindowsPowerShellCompatibilityModuleDenyList": ["' + $ModuleName2 + '"]}' | Out-File -Force $ConfigPath + $out = & $pwsh -NoProfile -NonInteractive -settingsFile $ConfigPath -c "[System.Management.Automation.Internal.InternalTestHooks]::SetTestHook('TestWindowsPowerShellPSHomeLocation', `'$basePath`');Import-Module $ModuleName2 -UseWindowsPowerShell -ErrorVariable z -ErrorAction SilentlyContinue;`$z.FullyQualifiedErrorId" + $out | Should -BeExactly 'Modules_ModuleInWinCompatDenyList,Microsoft.PowerShell.Commands.ImportModuleCommand' + } + + It "Blocks DenyList module import by Import-Module " { + '{"WindowsPowerShellCompatibilityModuleDenyList": ["' + $ModuleName2.ToLowerInvariant() + '"]}' | Out-File -Force $ConfigPath # also check case-insensitive comparison + $out = & $pwsh -NoProfile -NonInteractive -settingsFile $ConfigPath -c "[System.Management.Automation.Internal.InternalTestHooks]::SetTestHook('TestWindowsPowerShellPSHomeLocation', `'$basePath`');Import-Module $ModuleName2 -ErrorVariable z -ErrorAction SilentlyContinue;`$z.FullyQualifiedErrorId" + $out | Should -BeExactly 'Modules_ModuleInWinCompatDenyList,Microsoft.PowerShell.Commands.ImportModuleCommand' + } + + It "Blocks DenyList module import by CommandDiscovery\ModuleAutoload" { + '{"WindowsPowerShellCompatibilityModuleDenyList": ["RandomNameJustToMakeArrayOfSeveralModules","' + $ModuleName2 + '"]}' | Out-File -Force $ConfigPath + $out = & $pwsh -NoProfile -NonInteractive -settingsFile $ConfigPath -c "[System.Management.Automation.Internal.InternalTestHooks]::SetTestHook('TestWindowsPowerShellPSHomeLocation', `'$basePath`');`$ErrorActionPreference = 'SilentlyContinue';Test-$ModuleName2;`$error[0].FullyQualifiedErrorId" + $out | Should -BeExactly 'CouldNotAutoloadMatchingModule' + } + } } Describe "PSModulePath changes interacting with other PowerShell processes" -Tag "Feature" { @@ -513,7 +553,7 @@ Describe "PSModulePath changes interacting with other PowerShell processes" -Tag } } - <# Remove Pending status and update test after issue #11575 is fixed #> + # Remove Pending status and update test after issue #11575 is fixed It "Does not duplicate the System32 module path in subprocesses" -Pending:$true { $sys32ModPathCount = & $pwsh -C { & "$PSHOME/pwsh" -C '$null = $env:PSModulePath -match ([regex]::Escape((Join-Path $env:windir "System32" "WindowsPowerShell" "v1.0" "Modules"))); $matches.Count'