Add WinCompat deny list support using a setting in powershell.… (#11726)

This commit is contained in:
Andrew 2020-02-04 16:33:26 -08:00 committed by GitHub
parent bb021a977f
commit 43c88a4ff1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 139 additions and 8 deletions

View file

@ -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

View file

@ -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;

View file

@ -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>

View file

@ -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>

View file

@ -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'