Add WinCompat deny list support using a setting in powershell.… (#11726)
This commit is contained in:
parent
bb021a977f
commit
43c88a4ff1
|
@ -542,7 +542,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
|
||||
|
|
|
@ -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<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))
|
||||
{
|
||||
|
@ -1895,7 +1968,8 @@ namespace Microsoft.PowerShell.Commands
|
|||
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;
|
||||
|
|
|
@ -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<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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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" {
|
||||
|
@ -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'
|
||||
|
|
Loading…
Reference in a new issue