Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion build.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -537,7 +537,8 @@ Fix steps:
# publish powershell.config.json
$config = @{}
if ($environment.IsWindows) {
$config = @{ "Microsoft.PowerShell:ExecutionPolicy" = "RemoteSigned" }
$config = @{ "Microsoft.PowerShell:ExecutionPolicy" = "RemoteSigned";
"WindowsPowerShellCompatibilityModuleDenyList" = @("PSScheduledJob","BestPractices","UpdateServices") }
}

# When building preview, we want the configuration to enable all experiemental features by default
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -1878,10 +1880,81 @@ protected override void ProcessRecord()
}
}

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<T> FilterModuleCollection<T>(IEnumerable<T> moduleCollection)
{
List<T> 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<T>(moduleCollection);
}
else
{
filteredModuleCollection = new List<T>();
foreach (var module in moduleCollection)
{
if (!IsModuleInDenyList(moduleDenyList, module as string, module as ModuleSpecification))
{
filteredModuleCollection.Add(module);
}
}
}
}

return filteredModuleCollection;
}

internal override IList<PSModuleInfo> ImportModulesUsingWinCompat(IEnumerable<string> moduleNames, IEnumerable<ModuleSpecification> moduleFullyQualifiedNames, ImportModuleOptions importModuleOptions)
{
IList<PSModuleInfo> moduleProxyList = new List<PSModuleInfo>();
#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<string> filteredModuleNames = FilterModuleCollection(moduleNames);
List<ModuleSpecification> 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))
{
Expand All @@ -1895,7 +1968,8 @@ internal override IList<PSModuleInfo> ImportModulesUsingWinCompat(IEnumerable<st
return new List<PSModuleInfo>();
}

moduleProxyList = ImportModule_RemotelyViaPsrpSession(importModuleOptions, moduleNames, moduleFullyQualifiedNames, WindowsPowerShellCompatRemotingSession, usingWinCompat: true);
moduleProxyList = ImportModule_RemotelyViaPsrpSession(importModuleOptions, filteredModuleNames, filteredModuleFullyQualifiedNames, WindowsPowerShellCompatRemotingSession, usingWinCompat: true);

foreach (PSModuleInfo moduleProxy in moduleProxyList)
{
moduleProxy.IsWindowsPowerShellCompatModule = true;
Expand Down
13 changes: 13 additions & 0 deletions src/System.Management.Automation/engine/PSConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ internal sealed class PowerShellConfig
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();
Expand Down Expand Up @@ -227,6 +228,18 @@ internal bool IsImplicitWinCompatEnabled()
return !settingValue.Value;
}

internal string[] GetWindowsPowerShellCompatibilityModuleDenyList()
{
string[] settingValue = ReadValueFromFile<string[]>(ConfigScope.CurrentUser, WindowsPowerShellCompatibilityModuleDenyListKey);
if (settingValue == null)
{
// if the setting is not mentioned in configuration files, then the default WindowsPowerShellCompatibilityModuleDenyList value is null
settingValue = ReadValueFromFile<string[]>(ConfigScope.AllUsers, WindowsPowerShellCompatibilityModuleDenyListKey);
}

return settingValue;
}

/// <summary>
/// Corresponding settings of the original Group Policies.
/// </summary>
Expand Down
3 changes: 3 additions & 0 deletions src/System.Management.Automation/resources/Modules.resx
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,9 @@
<data name="WinCompatRequredVersionError" xml:space="preserve">
<value>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.</value>
</data>
<data name="WinCompatModuleInDenyList" xml:space="preserve">
<value>Module '{0}' is blocked from loading using Windows PowerShell compatibility feature by a 'WindowsPowerShellCompatibilityModuleDenyList' setting in PowerShell configuration file.</value>
</data>
<data name="PsModuleOverCimSessionError" xml:space="preserve">
<value>The module {0} cannot be imported over a CimSession. Try using the PSSession parameter of the Import-Module cmdlet.</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <ModuleName> -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 <ModuleName>" {
'{"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" {
Expand Down Expand Up @@ -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'
Expand Down