diff --git a/doBuild.ps1 b/doBuild.ps1
index 6eab1d609..15293d99b 100644
--- a/doBuild.ps1
+++ b/doBuild.ps1
@@ -34,9 +34,19 @@ function DoBuild
Copy-Item -Path "./LICENSE" -Dest "$BuildOutPath"
# Copy notice
- Write-Verbose -Verbose -Message "Copying ThirdPartyNotices.txt to '$BuildOutPath'"
+ Write-Verbose -Verbose -Message "Copying Notice.txt to '$BuildOutPath'"
Copy-Item -Path "./Notice.txt" -Dest "$BuildOutPath"
+ # Copy Group Policy files
+ Write-Verbose -Verbose -Message "Copying InstallPSResourceGetPolicyDefinitions.ps1 to '$BuildOutPath'"
+ Copy-Item -Path "${SrcPath}/InstallPSResourceGetPolicyDefinitions.ps1" -Dest "$BuildOutPath" -Force
+
+ Write-Verbose -Verbose -Message "Copying PSResourceRepository.adml to '$BuildOutPath'"
+ Copy-Item -Path "${SrcPath}/PSResourceRepository.adml" -Dest "$BuildOutPath" -Force
+
+ Write-Verbose -Verbose -Message "Copying PSResourceRepository.admx to '$BuildOutPath'"
+ Copy-Item -Path "${SrcPath}/PSResourceRepository.admx" -Dest "$BuildOutPath" -Force
+
# Build and place binaries
if ( Test-Path "${SrcPath}/code" ) {
Write-Verbose -Verbose -Message "Building assembly and copying to '$BuildOutPath'"
diff --git a/global.json b/global.json
index 8acf2f3a1..120c43985 100644
--- a/global.json
+++ b/global.json
@@ -1,5 +1,5 @@
{
"sdk": {
- "version": "8.0.400"
+ "version": "8.0.403"
}
}
diff --git a/src/InstallPSResourceGetPolicyDefinitions.ps1 b/src/InstallPSResourceGetPolicyDefinitions.ps1
new file mode 100644
index 000000000..e0f2d15d4
--- /dev/null
+++ b/src/InstallPSResourceGetPolicyDefinitions.ps1
@@ -0,0 +1,88 @@
+# Copyright (c) Microsoft Corporation.
+# Licensed under the MIT License.
+
+<#
+.Synopsis
+ Group Policy tools use administrative template files (.admx, .adml) to populate policy settings in the user interface.
+ This allows administrators to manage registry-based policy settings.
+ This script installs PSResourceGet Administrative Templates for Windows.
+.Notes
+ The PSResourceRepository.admx and PSResourceRepository.adml files are
+ expected to be at the location specified by the Path parameter with default value of the location of this script.
+#>
+[CmdletBinding()]
+param
+(
+ [ValidateNotNullOrEmpty()]
+ [string] $Path = $PSScriptRoot
+)
+Set-StrictMode -Version 3.0
+$ErrorActionPreference = 'Stop'
+
+function Test-Elevated
+{
+ [CmdletBinding()]
+ [OutputType([bool])]
+ Param()
+
+ # if the current Powershell session was called with administrator privileges,
+ # the Administrator Group's well-known SID will show up in the Groups for the current identity.
+ # Note that the SID won't show up unless the process is elevated.
+ return (([Security.Principal.WindowsIdentity]::GetCurrent()).Groups -contains "S-1-5-32-544")
+}
+$IsWindowsOs = $PSHOME.EndsWith('\WindowsPowerShell\v1.0', [System.StringComparison]::OrdinalIgnoreCase) -or $IsWindows
+
+if (-not $IsWindowsOs)
+{
+ throw 'This script must be run on Windows.'
+}
+
+if (-not (Test-Elevated))
+{
+ throw 'This script must be run from an elevated process.'
+}
+
+if ([System.Management.Automation.Platform]::IsNanoServer)
+{
+ throw 'Group policy definitions are not supported on Nano Server.'
+}
+
+$admxName = 'PSResourceRepository.admx'
+$admlName = 'PSResourceRepository.adml'
+$admx = Get-Item -Path (Join-Path -Path $Path -ChildPath $admxName)
+$adml = Get-Item -Path (Join-Path -Path $Path -ChildPath $admlName)
+$admxTargetPath = Join-Path -Path $env:WINDIR -ChildPath "PolicyDefinitions"
+$admlTargetPath = Join-Path -Path $admxTargetPath -ChildPath "en-US"
+
+$files = @($admx, $adml)
+foreach ($file in $files)
+{
+ if (-not (Test-Path -Path $file))
+ {
+ throw "Could not find $($file.Name) at $Path"
+ }
+}
+
+Write-Verbose "Copying $admx to $admxTargetPath"
+Copy-Item -Path $admx -Destination $admxTargetPath -Force
+$admxTargetFullPath = Join-Path -Path $admxTargetPath -ChildPath $admxName
+if (Test-Path -Path $admxTargetFullPath)
+{
+ Write-Verbose "$admxName was installed successfully"
+}
+else
+{
+ Write-Error "Could not install $admxName"
+}
+
+Write-Verbose "Copying $adml to $admlTargetPath"
+Copy-Item -Path $adml -Destination $admlTargetPath -Force
+$admlTargetFullPath = Join-Path -Path $admlTargetPath -ChildPath $admlName
+if (Test-Path -Path $admlTargetFullPath)
+{
+ Write-Verbose "$admlName was installed successfully"
+}
+else
+{
+ Write-Error "Could not install $admlName"
+}
diff --git a/src/PSGet.Format.ps1xml b/src/PSGet.Format.ps1xml
index 18140432b..e81b0728a 100644
--- a/src/PSGet.Format.ps1xml
+++ b/src/PSGet.Format.ps1xml
@@ -94,6 +94,7 @@
+
@@ -102,6 +103,7 @@
Uri
Trusted
Priority
+ IsAllowedByPolicy
diff --git a/src/PSResourceRepository.adml b/src/PSResourceRepository.adml
new file mode 100644
index 000000000..5da96427f
--- /dev/null
+++ b/src/PSResourceRepository.adml
@@ -0,0 +1,20 @@
+
+
+
+ PSResourceGet Repository Policy
+ This creates an allow list of repositories for PSResourceGet.
+
+
+ At least Windows 11*
+ PSResourceGet Repository Policy
+ This creates an allow list of repositories for PSResourceGet.
+ PSResourceGet Repository Policies
+
+
+
+ Please create an allow list of repositories using a name value pair like following: Name=PSGallery;Uri=https://www.powershellgallery.com/api/v2
+
+
+
+
+
diff --git a/src/PSResourceRepository.admx b/src/PSResourceRepository.admx
new file mode 100644
index 000000000..6e8db3ec6
--- /dev/null
+++ b/src/PSResourceRepository.admx
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/code/FindHelper.cs b/src/code/FindHelper.cs
index a2a14fa72..327d0e024 100644
--- a/src/code/FindHelper.cs
+++ b/src/code/FindHelper.cs
@@ -9,6 +9,7 @@
using System.Management.Automation;
using System.Net;
using System.Runtime.ExceptionServices;
+using System.Text.RegularExpressions;
using System.Threading;
namespace Microsoft.PowerShell.PSResourceGet.Cmdlets
@@ -182,9 +183,24 @@ public IEnumerable FindByResourceName(
}
List repositoryNamesToSearch = new List();
+
for (int i = 0; i < repositoriesToSearch.Count; i++)
{
PSRepositoryInfo currentRepository = repositoriesToSearch[i];
+
+ bool isAllowed = GroupPolicyRepositoryEnforcement.IsRepositoryAllowed(currentRepository.Uri);
+
+ if (!isAllowed)
+ {
+ _cmdletPassedIn.WriteError(new ErrorRecord(
+ new PSInvalidOperationException($"Repository '{currentRepository.Name}' is not allowed by Group Policy."),
+ "RepositoryNotAllowedByGroupPolicy",
+ ErrorCategory.PermissionDenied,
+ this));
+
+ continue;
+ }
+
repositoryNamesToSearch.Add(currentRepository.Name);
_networkCredential = Utils.SetNetworkCredential(currentRepository, _networkCredential, _cmdletPassedIn);
ServerApiCall currentServer = ServerFactory.GetServer(currentRepository, _cmdletPassedIn, _networkCredential);
@@ -230,7 +246,7 @@ public IEnumerable FindByResourceName(
// Scenarios: Find-PSResource -Name "pkg" -Repository *Gallery -> write error if only if pkg wasn't found in any matching repositories.
foreach(string pkgName in pkgsDiscovered)
{
- var msg = repository == null ? $"Package '{pkgName}' could not be found in any registered repositories." :
+ var msg = repository == null ? $"Package '{pkgName}' could not be found in any registered repositories." :
$"Package '{pkgName}' could not be found in registered repositories: '{string.Join(", ", repositoryNamesToSearch)}'.";
_cmdletPassedIn.WriteError(new ErrorRecord(
@@ -355,6 +371,20 @@ public IEnumerable FindByCommandOrDscResource(
for (int i = 0; i < repositoriesToSearch.Count; i++)
{
PSRepositoryInfo currentRepository = repositoriesToSearch[i];
+
+ bool isAllowed = GroupPolicyRepositoryEnforcement.IsRepositoryAllowed(currentRepository.Uri);
+
+ if (!isAllowed)
+ {
+ _cmdletPassedIn.WriteError(new ErrorRecord(
+ new PSInvalidOperationException($"Repository '{currentRepository.Name}' is not allowed by Group Policy."),
+ "RepositoryNotAllowedByGroupPolicy",
+ ErrorCategory.PermissionDenied,
+ this));
+
+ continue;
+ }
+
repositoryNamesToSearch.Add(currentRepository.Name);
_networkCredential = Utils.SetNetworkCredential(currentRepository, _networkCredential, _cmdletPassedIn);
ServerApiCall currentServer = ServerFactory.GetServer(currentRepository, _cmdletPassedIn, _networkCredential);
@@ -393,9 +423,9 @@ public IEnumerable FindByCommandOrDscResource(
if (currentResult.exception != null && !currentResult.exception.Message.Equals(string.Empty))
{
errRecord = new ErrorRecord(
- new ResourceNotFoundException($"'{String.Join(", ", _tag)}' could not be found", currentResult.exception),
- "FindCmdOrDscToPSResourceObjFailure",
- ErrorCategory.NotSpecified,
+ new ResourceNotFoundException($"'{String.Join(", ", _tag)}' could not be found", currentResult.exception),
+ "FindCmdOrDscToPSResourceObjFailure",
+ ErrorCategory.NotSpecified,
this);
if (shouldReportErrorForEachRepo)
@@ -421,7 +451,7 @@ public IEnumerable FindByCommandOrDscResource(
if (!isCmdOrDSCTagFound && !shouldReportErrorForEachRepo)
{
string parameterName = isSearchingForCommands ? "CommandName" : "DSCResourceName";
- var msg = repository == null ? $"Package with {parameterName} '{String.Join(", ", _tag)}' could not be found in any registered repositories." :
+ var msg = repository == null ? $"Package with {parameterName} '{String.Join(", ", _tag)}' could not be found in any registered repositories." :
$"Package with {parameterName} '{String.Join(", ", _tag)}' could not be found in registered repositories: '{string.Join(", ", repositoryNamesToSearch)}'.";
_cmdletPassedIn.WriteError(new ErrorRecord(
@@ -545,6 +575,20 @@ public IEnumerable FindByTag(
for (int i = 0; i < repositoriesToSearch.Count; i++)
{
PSRepositoryInfo currentRepository = repositoriesToSearch[i];
+
+ bool isAllowed = GroupPolicyRepositoryEnforcement.IsRepositoryAllowed(currentRepository.Uri);
+
+ if (!isAllowed)
+ {
+ _cmdletPassedIn.WriteError(new ErrorRecord(
+ new PSInvalidOperationException($"Repository '{currentRepository.Name}' is not allowed by Group Policy."),
+ "RepositoryNotAllowedByGroupPolicy",
+ ErrorCategory.PermissionDenied,
+ this));
+
+ continue;
+ }
+
repositoryNamesToSearch.Add(currentRepository.Name);
_networkCredential = Utils.SetNetworkCredential(currentRepository, _networkCredential, _cmdletPassedIn);
ServerApiCall currentServer = ServerFactory.GetServer(currentRepository, _cmdletPassedIn, _networkCredential);
@@ -591,9 +635,9 @@ public IEnumerable FindByTag(
if (currentResult.exception != null && !currentResult.exception.Message.Equals(string.Empty))
{
errRecord = new ErrorRecord(
- new ResourceNotFoundException($"Tags '{String.Join(", ", _tag)}' could not be found" , currentResult.exception),
- "FindTagConvertToPSResourceFailure",
- ErrorCategory.InvalidResult,
+ new ResourceNotFoundException($"Tags '{String.Join(", ", _tag)}' could not be found" , currentResult.exception),
+ "FindTagConvertToPSResourceFailure",
+ ErrorCategory.InvalidResult,
this);
if (shouldReportErrorForEachRepo)
@@ -615,7 +659,7 @@ public IEnumerable FindByTag(
if (!isTagFound && !shouldReportErrorForEachRepo)
{
- var msg = repository == null ? $"Package with Tags '{String.Join(", ", _tag)}' could not be found in any registered repositories." :
+ var msg = repository == null ? $"Package with Tags '{String.Join(", ", _tag)}' could not be found in any registered repositories." :
$"Package with Tags '{String.Join(", ", _tag)}' could not be found in registered repositories: '{string.Join(", ", repositoryNamesToSearch)}'.";
_cmdletPassedIn.WriteError(new ErrorRecord(
@@ -665,9 +709,9 @@ private IEnumerable SearchByNames(ServerApiCall currentServer, R
if (currentResult.exception != null && !currentResult.exception.Message.Equals(string.Empty))
{
_cmdletPassedIn.WriteError(new ErrorRecord(
- currentResult.exception,
- "FindAllConvertToPSResourceFailure",
- ErrorCategory.InvalidResult,
+ currentResult.exception,
+ "FindAllConvertToPSResourceFailure",
+ ErrorCategory.InvalidResult,
this));
continue;
@@ -721,9 +765,9 @@ private IEnumerable SearchByNames(ServerApiCall currentServer, R
if (currentResult.exception != null && !currentResult.exception.Message.Equals(string.Empty))
{
_cmdletPassedIn.WriteError(new ErrorRecord(
- currentResult.exception,
- "FindNameGlobbingConvertToPSResourceFailure",
- ErrorCategory.InvalidResult,
+ currentResult.exception,
+ "FindNameGlobbingConvertToPSResourceFailure",
+ ErrorCategory.InvalidResult,
this));
continue;
@@ -772,9 +816,9 @@ private IEnumerable SearchByNames(ServerApiCall currentServer, R
{
// This scenario may occur when the package version requested is unlisted.
_cmdletPassedIn.WriteError(new ErrorRecord(
- new ResourceNotFoundException($"Package with name '{pkgName}'{tagsAsString} could not be found in repository '{repository.Name}'"),
- "PackageNotFound",
- ErrorCategory.ObjectNotFound,
+ new ResourceNotFoundException($"Package with name '{pkgName}'{tagsAsString} could not be found in repository '{repository.Name}'"),
+ "PackageNotFound",
+ ErrorCategory.ObjectNotFound,
this));
continue;
@@ -783,9 +827,9 @@ private IEnumerable SearchByNames(ServerApiCall currentServer, R
if (currentResult.exception != null && !currentResult.exception.Message.Equals(string.Empty))
{
_cmdletPassedIn.WriteError(new ErrorRecord(
- currentResult.exception,
- "FindNameConvertToPSResourceFailure",
- ErrorCategory.ObjectNotFound,
+ currentResult.exception,
+ "FindNameConvertToPSResourceFailure",
+ ErrorCategory.ObjectNotFound,
this));
continue;
@@ -804,8 +848,8 @@ private IEnumerable SearchByNames(ServerApiCall currentServer, R
{
_cmdletPassedIn.WriteError(new ErrorRecord(
new ArgumentException("Name cannot contain or equal wildcard when using specific version."),
- "InvalidWildCardUsage",
- ErrorCategory.InvalidOperation,
+ "InvalidWildCardUsage",
+ ErrorCategory.InvalidOperation,
this));
continue;
@@ -846,9 +890,9 @@ private IEnumerable SearchByNames(ServerApiCall currentServer, R
{
// This scenario may occur when the package version requested is unlisted.
_cmdletPassedIn.WriteError(new ErrorRecord(
- new ResourceNotFoundException($"Package with name '{pkgName}', version '{_nugetVersion.ToNormalizedString()}'{tagsAsString} could not be found in repository '{repository.Name}'"),
- "PackageNotFound",
- ErrorCategory.ObjectNotFound,
+ new ResourceNotFoundException($"Package with name '{pkgName}', version '{_nugetVersion.ToNormalizedString()}'{tagsAsString} could not be found in repository '{repository.Name}'"),
+ "PackageNotFound",
+ ErrorCategory.ObjectNotFound,
this));
continue;
@@ -858,8 +902,8 @@ private IEnumerable SearchByNames(ServerApiCall currentServer, R
{
_cmdletPassedIn.WriteError(new ErrorRecord(
currentResult.exception,
- "FindVersionConvertToPSResourceFailure",
- ErrorCategory.ObjectNotFound,
+ "FindVersionConvertToPSResourceFailure",
+ ErrorCategory.ObjectNotFound,
this));
continue;
@@ -879,8 +923,8 @@ private IEnumerable SearchByNames(ServerApiCall currentServer, R
{
_cmdletPassedIn.WriteError(new ErrorRecord(
new ArgumentException("Name cannot contain or equal wildcard when using version range"),
- "InvalidWildCardUsage",
- ErrorCategory.InvalidOperation,
+ "InvalidWildCardUsage",
+ ErrorCategory.InvalidOperation,
this));
}
else
@@ -897,7 +941,7 @@ private IEnumerable SearchByNames(ServerApiCall currentServer, R
{
_cmdletPassedIn.WriteError(new ErrorRecord(
new ArgumentException("-Tag parameter cannot be specified when using version range."),
- "InvalidWildCardUsage",
+ "InvalidWildCardUsage",
ErrorCategory.InvalidOperation,
this));
@@ -925,14 +969,14 @@ private IEnumerable SearchByNames(ServerApiCall currentServer, R
{
_cmdletPassedIn.WriteError(new ErrorRecord(
currentResult.exception,
- "FindVersionGlobbingConvertToPSResourceFailure",
- ErrorCategory.ObjectNotFound,
+ "FindVersionGlobbingConvertToPSResourceFailure",
+ ErrorCategory.ObjectNotFound,
this));
continue;
}
- // Check to see if version falls within version range
+ // Check to see if version falls within version range
PSResourceInfo foundPkg = currentResult.returnedObject;
string versionStr = $"{foundPkg.Version}";
if (foundPkg.IsPrerelease)
@@ -1014,7 +1058,7 @@ private bool TryAddToPackagesFound(PSResourceInfo foundPkg)
_packagesFound.Add(foundPkg.Name, new List { foundPkgVersion });
addedToHash = true;
}
-
+
_cmdletPassedIn.WriteDebug($"Found package '{foundPkg.Name}' version '{foundPkg.Version}'");
return addedToHash;
@@ -1070,9 +1114,9 @@ internal IEnumerable FindDependencyPackages(
{
// This scenario may occur when the package version requested is unlisted.
_cmdletPassedIn.WriteError(new ErrorRecord(
- new ResourceNotFoundException($"Dependency package with name '{dep.Name}' could not be found in repository '{repository.Name}'"),
- "DependencyPackageNotFound",
- ErrorCategory.ObjectNotFound,
+ new ResourceNotFoundException($"Dependency package with name '{dep.Name}' could not be found in repository '{repository.Name}'"),
+ "DependencyPackageNotFound",
+ ErrorCategory.ObjectNotFound,
this));
yield return null;
continue;
@@ -1081,9 +1125,9 @@ internal IEnumerable FindDependencyPackages(
if (currentResult.exception != null && !currentResult.exception.Message.Equals(string.Empty))
{
_cmdletPassedIn.WriteError(new ErrorRecord(
- new ResourceNotFoundException($"Dependency package with name '{dep.Name}' could not be found in repository '{repository.Name}'", currentResult.exception),
- "DependencyPackageNotFound",
- ErrorCategory.ObjectNotFound,
+ new ResourceNotFoundException($"Dependency package with name '{dep.Name}' could not be found in repository '{repository.Name}'", currentResult.exception),
+ "DependencyPackageNotFound",
+ ErrorCategory.ObjectNotFound,
this));
yield return null;
continue;
@@ -1130,9 +1174,9 @@ internal IEnumerable FindDependencyPackages(
if (responses.IsFindResultsEmpty())
{
_cmdletPassedIn.WriteError(new ErrorRecord(
- new InvalidOrEmptyResponse($"Dependency package with name {dep.Name} and version range {dep.VersionRange} could not be found in repository '{repository.Name}"),
- "FindDepPackagesFindVersionGlobbingFailure",
- ErrorCategory.InvalidResult,
+ new InvalidOrEmptyResponse($"Dependency package with name {dep.Name} and version range {dep.VersionRange} could not be found in repository '{repository.Name}"),
+ "FindDepPackagesFindVersionGlobbingFailure",
+ ErrorCategory.InvalidResult,
this));
yield return null;
continue;
@@ -1143,16 +1187,16 @@ internal IEnumerable FindDependencyPackages(
if (currentResult.exception != null && !currentResult.exception.Message.Equals(string.Empty))
{
_cmdletPassedIn.WriteError(new ErrorRecord(
- new ResourceNotFoundException($"Dependency package with name '{dep.Name}' and version range '{dep.VersionRange}' could not be found in repository '{repository.Name}'", currentResult.exception),
- "DependencyPackageNotFound",
- ErrorCategory.ObjectNotFound,
+ new ResourceNotFoundException($"Dependency package with name '{dep.Name}' and version range '{dep.VersionRange}' could not be found in repository '{repository.Name}'", currentResult.exception),
+ "DependencyPackageNotFound",
+ ErrorCategory.ObjectNotFound,
this));
-
+
yield return null;
continue;
}
- // Check to see if version falls within version range
+ // Check to see if version falls within version range
PSResourceInfo foundDep = currentResult.returnedObject;
string depVersionStr = $"{foundDep.Version}";
if (foundDep.IsPrerelease) {
diff --git a/src/code/GroupPolicyRepositoryEnforcement.cs b/src/code/GroupPolicyRepositoryEnforcement.cs
new file mode 100644
index 000000000..ac4f7ee98
--- /dev/null
+++ b/src/code/GroupPolicyRepositoryEnforcement.cs
@@ -0,0 +1,226 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+#nullable enable
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.PowerShell.PSResourceGet.UtilClasses;
+using Microsoft.Win32;
+
+namespace Microsoft.PowerShell.PSResourceGet.Cmdlets
+{
+ ///
+ /// This class is used to enforce group policy for repositories.
+ ///
+ public class GroupPolicyRepositoryEnforcement
+ {
+ const string userRoot = "HKEY_CURRENT_USER";
+ const string psresourcegetGPPath = @"SOFTWARE\Policies\Microsoft\PSResourceGetRepository";
+ const string gpRootPath = @"Software\Microsoft\Windows\CurrentVersion\Group Policy Objects";
+
+ private GroupPolicyRepositoryEnforcement()
+ {
+ }
+
+ ///
+ /// This method is used to see if the group policy is enabled.
+ ///
+ ///
+ /// True if the group policy is enabled, false otherwise.
+ public static bool IsGroupPolicyEnabled()
+ {
+ if (Environment.OSVersion.Platform != PlatformID.Win32NT)
+ {
+ // Always return false for non-Windows platforms and Group Policy is not available.
+ return false;
+ }
+
+ if (InternalHooks.EnableGPRegistryHook)
+ {
+ return InternalHooks.GPEnabledStatus;
+ }
+
+ var values = ReadGPFromRegistry();
+
+ if (values is not null && values.Count > 0)
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ ///
+ /// Get allowed list of URIs for allowed repositories.
+ ///
+ /// Array of allowed URIs.
+ /// Thrown when the group policy is not enabled.
+ public static Uri[]? GetAllowedRepositoryURIs()
+ {
+ if (Environment.OSVersion.Platform != PlatformID.Win32NT)
+ {
+ throw new PlatformNotSupportedException("Group policy is only supported on Windows.");
+ }
+
+ if (InternalHooks.EnableGPRegistryHook)
+ {
+ var uri = new Uri(InternalHooks.AllowedUri);
+ return new Uri[] { uri };
+ }
+
+ if (!IsGroupPolicyEnabled())
+ {
+ return null;
+ }
+ else
+ {
+ List allowedUris = new List();
+
+ var allowedRepositories = ReadGPFromRegistry();
+
+ if (allowedRepositories is not null && allowedRepositories.Count > 0)
+ {
+ foreach (var allowedRepository in allowedRepositories)
+ {
+ allowedUris.Add(allowedRepository.Value);
+ }
+ }
+
+ return allowedUris.ToArray();
+ }
+ }
+
+ internal static bool IsRepositoryAllowed(Uri repositoryUri)
+ {
+ bool isAllowed = false;
+
+ if(GroupPolicyRepositoryEnforcement.IsGroupPolicyEnabled())
+ {
+ var allowedList = GroupPolicyRepositoryEnforcement.GetAllowedRepositoryURIs();
+
+ if (allowedList != null && allowedList.Length > 0)
+ {
+ isAllowed = allowedList.Any(uri => uri.Equals(repositoryUri));
+ }
+ }
+ else
+ {
+ isAllowed = true;
+ }
+
+ return isAllowed;
+ }
+
+ private static List>? ReadGPFromRegistry()
+ {
+ List> allowedRepositories = new List>();
+
+ using (var key = Registry.CurrentUser.OpenSubKey(gpRootPath))
+ {
+ if (key is null)
+ {
+ return null;
+ }
+
+ var subKeys = key.GetSubKeyNames();
+
+ if (subKeys is null)
+ {
+ return null;
+ }
+
+ foreach (var subKey in subKeys)
+ {
+ if (subKey.EndsWith("Machine"))
+ {
+ continue;
+ }
+
+ using (var psrgKey = key.OpenSubKey(subKey + "\\" + psresourcegetGPPath))
+ {
+ if (psrgKey is null)
+ {
+ // this GPO does not have PSResourceGetRepository key
+ continue;
+ }
+
+ var valueNames = psrgKey.GetValueNames();
+
+ // This means it is disabled
+ if (valueNames is null || valueNames.Length == 0 || valueNames.Length == 1 && valueNames[0].Equals("**delvals.", StringComparison.OrdinalIgnoreCase))
+ {
+ return null;
+ }
+ else
+ {
+ foreach (var valueName in valueNames)
+ {
+ if (valueName.Equals("**delvals.", StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+
+ var value = psrgKey.GetValue(valueName);
+
+ if (value is null)
+ {
+ throw new InvalidOperationException("Invalid registry value.");
+ }
+
+ string valueString = value.ToString();
+ var kvRegValue = ConvertRegValue(valueString);
+ allowedRepositories.Add(kvRegValue);
+ }
+ }
+ }
+ }
+ }
+
+ return allowedRepositories;
+ }
+
+ private static KeyValuePair ConvertRegValue(string regValue)
+ {
+ if (string.IsNullOrEmpty(regValue))
+ {
+ throw new ArgumentException("Registry value is empty.");
+ }
+
+ var KvPairs = regValue.Split(new char[] { ';' });
+
+ string? nameValue = null;
+ string? uriValue = null;
+
+ foreach (var kvPair in KvPairs)
+ {
+ var kv = kvPair.Split(new char[] { '=' }, 2);
+
+ if (kv.Length != 2)
+ {
+ throw new InvalidOperationException("Invalid registry value.");
+ }
+
+ if (kv[0].Equals("Name", StringComparison.OrdinalIgnoreCase))
+ {
+ nameValue = kv[1];
+ }
+
+ if (kv[0].Equals("Uri", StringComparison.OrdinalIgnoreCase))
+ {
+ uriValue = kv[1];
+ }
+ }
+
+ if (nameValue is not null && uriValue is not null)
+ {
+ return new KeyValuePair(nameValue, new Uri(uriValue));
+ }
+ else
+ {
+ throw new InvalidOperationException("Invalid registry value.");
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/code/InstallHelper.cs b/src/code/InstallHelper.cs
index 8cc212eb7..b916268c2 100644
--- a/src/code/InstallHelper.cs
+++ b/src/code/InstallHelper.cs
@@ -269,6 +269,20 @@ private List ProcessRepositories(
for (int i = 0; i < listOfRepositories.Count && _pkgNamesToInstall.Count > 0; i++)
{
PSRepositoryInfo currentRepository = listOfRepositories[i];
+
+ bool isAllowed = GroupPolicyRepositoryEnforcement.IsRepositoryAllowed(currentRepository.Uri);
+
+ if (!isAllowed)
+ {
+ _cmdletPassedIn.WriteError(new ErrorRecord(
+ new PSInvalidOperationException($"Repository '{currentRepository.Name}' is not allowed by Group Policy."),
+ "RepositoryNotAllowedByGroupPolicy",
+ ErrorCategory.PermissionDenied,
+ this));
+
+ continue;
+ }
+
string repoName = currentRepository.Name;
sourceTrusted = currentRepository.Trusted || trustRepository;
diff --git a/src/code/InternalHooks.cs b/src/code/InternalHooks.cs
index 9b5ea0294..2078d1d41 100644
--- a/src/code/InternalHooks.cs
+++ b/src/code/InternalHooks.cs
@@ -9,6 +9,12 @@ public class InternalHooks
{
internal static bool InvokedFromCompat;
+ internal static bool EnableGPRegistryHook;
+
+ internal static bool GPEnabledStatus;
+
+ internal static string AllowedUri;
+
public static void SetTestHook(string property, object value)
{
var fieldInfo = typeof(InternalHooks).GetField(property, BindingFlags.Static | BindingFlags.NonPublic);
diff --git a/src/code/PSRepositoryInfo.cs b/src/code/PSRepositoryInfo.cs
index 7faa02bc4..b660c6690 100644
--- a/src/code/PSRepositoryInfo.cs
+++ b/src/code/PSRepositoryInfo.cs
@@ -27,7 +27,7 @@ public enum APIVersion
#region Constructor
- public PSRepositoryInfo(string name, Uri uri, int priority, bool trusted, PSCredentialInfo credentialInfo, APIVersion apiVersion)
+ public PSRepositoryInfo(string name, Uri uri, int priority, bool trusted, PSCredentialInfo credentialInfo, APIVersion apiVersion, bool allowed)
{
Name = name;
Uri = uri;
@@ -35,6 +35,7 @@ public PSRepositoryInfo(string name, Uri uri, int priority, bool trusted, PSCred
Trusted = trusted;
CredentialInfo = credentialInfo;
ApiVersion = apiVersion;
+ IsAllowedByPolicy = allowed;
}
#endregion
@@ -88,6 +89,11 @@ public enum RepositoryProviderType
///
public APIVersion ApiVersion { get; }
+ //
+ /// is it allowed by policy
+ ///
+ public bool IsAllowedByPolicy { get; set; }
+
#endregion
}
}
diff --git a/src/code/PublishHelper.cs b/src/code/PublishHelper.cs
index 6e15dd459..5470da611 100644
--- a/src/code/PublishHelper.cs
+++ b/src/code/PublishHelper.cs
@@ -355,6 +355,19 @@ internal void PushResource(string Repository, string modulePrefix, bool SkipDepe
return;
}
+ bool isAllowed = GroupPolicyRepositoryEnforcement.IsRepositoryAllowed(repository.Uri);
+
+ if (!isAllowed)
+ {
+ _cmdletPassedIn.WriteError(new ErrorRecord(
+ new PSInvalidOperationException($"Repository '{repository.Name}' is not allowed by Group Policy."),
+ "RepositoryNotAllowedByGroupPolicy",
+ ErrorCategory.PermissionDenied,
+ this));
+
+ return;
+ }
+
_networkCredential = Utils.SetNetworkCredential(repository, _networkCredential, _cmdletPassedIn);
// Check if dependencies already exist within the repo if:
diff --git a/src/code/RepositorySettings.cs b/src/code/RepositorySettings.cs
index b42e7e581..97e9f80b7 100644
--- a/src/code/RepositorySettings.cs
+++ b/src/code/RepositorySettings.cs
@@ -9,6 +9,7 @@
using System.Management.Automation;
using System.Xml;
using System.Xml.Linq;
+using Microsoft.PowerShell.PSResourceGet.Cmdlets;
using static Microsoft.PowerShell.PSResourceGet.UtilClasses.PSRepositoryInfo;
namespace Microsoft.PowerShell.PSResourceGet.UtilClasses
@@ -279,7 +280,9 @@ public static PSRepositoryInfo Add(string repoName, Uri repoUri, int repoPriorit
throw new PSInvalidOperationException(String.Format("Adding to repository store failed: {0}", e.Message));
}
- return new PSRepositoryInfo(repoName, repoUri, repoPriority, repoTrusted, repoCredentialInfo, apiVersion);
+ bool isAllowed = GroupPolicyRepositoryEnforcement.IsGroupPolicyEnabled() ? GroupPolicyRepositoryEnforcement.IsRepositoryAllowed(repoUri) : true;
+
+ return new PSRepositoryInfo(repoName, repoUri, repoPriority, repoTrusted, repoCredentialInfo, apiVersion, isAllowed);
}
///
@@ -436,13 +439,22 @@ public static PSRepositoryInfo Update(string repoName, Uri repoUri, int repoPrio
node.Attribute(PSCredentialInfo.SecretNameAttribute).Value);
}
+ if (GroupPolicyRepositoryEnforcement.IsGroupPolicyEnabled())
+ {
+ var allowedList = GroupPolicyRepositoryEnforcement.GetAllowedRepositoryURIs();
+
+ }
+
+ bool isAllowed = GroupPolicyRepositoryEnforcement.IsGroupPolicyEnabled() ? GroupPolicyRepositoryEnforcement.IsRepositoryAllowed(thisUrl) : true;
+
RepositoryProviderType repositoryProvider= GetRepositoryProviderType(thisUrl);
updatedRepo = new PSRepositoryInfo(repoName,
thisUrl,
Int32.Parse(node.Attribute("Priority").Value),
Boolean.Parse(node.Attribute("Trusted").Value),
thisCredentialInfo,
- resolvedAPIVersion);
+ resolvedAPIVersion,
+ isAllowed);
// Close the file
root.Save(FullRepositoryPath);
@@ -522,6 +534,9 @@ public static List Remove(string[] repoNames, out string[] err
string attributeUrlUriName = urlAttributeExists ? "Url" : "Uri";
Uri repoUri = new Uri(node.Attribute(attributeUrlUriName).Value);
+
+ bool isAllowed = GroupPolicyRepositoryEnforcement.IsGroupPolicyEnabled() ? GroupPolicyRepositoryEnforcement.IsRepositoryAllowed(repoUri) : true;
+
RepositoryProviderType repositoryProvider= GetRepositoryProviderType(repoUri);
removedRepos.Add(
new PSRepositoryInfo(repo,
@@ -529,7 +544,8 @@ public static List Remove(string[] repoNames, out string[] err
Int32.Parse(node.Attribute("Priority").Value),
Boolean.Parse(node.Attribute("Trusted").Value),
repoCredentialInfo,
- (PSRepositoryInfo.APIVersion)Enum.Parse(typeof(PSRepositoryInfo.APIVersion), node.Attribute("APIVersion").Value, ignoreCase: true)));
+ (PSRepositoryInfo.APIVersion)Enum.Parse(typeof(PSRepositoryInfo.APIVersion), node.Attribute("APIVersion").Value, ignoreCase: true),
+ isAllowed));
// Remove item from file
node.Remove();
@@ -654,12 +670,16 @@ public static List Read(string[] repoNames, out string[] error
}
RepositoryProviderType repositoryProvider= GetRepositoryProviderType(thisUrl);
+
+ bool isAllowed = GroupPolicyRepositoryEnforcement.IsGroupPolicyEnabled() ? GroupPolicyRepositoryEnforcement.IsRepositoryAllowed(thisUrl) : true;
+
PSRepositoryInfo currentRepoItem = new PSRepositoryInfo(repo.Attribute("Name").Value,
thisUrl,
Int32.Parse(repo.Attribute("Priority").Value),
Boolean.Parse(repo.Attribute("Trusted").Value),
thisCredentialInfo,
- (PSRepositoryInfo.APIVersion)Enum.Parse(typeof(PSRepositoryInfo.APIVersion), repo.Attribute("APIVersion").Value, ignoreCase: true));
+ (PSRepositoryInfo.APIVersion)Enum.Parse(typeof(PSRepositoryInfo.APIVersion), repo.Attribute("APIVersion").Value, ignoreCase: true),
+ isAllowed);
foundRepos.Add(currentRepoItem);
}
@@ -758,12 +778,16 @@ public static List Read(string[] repoNames, out string[] error
}
RepositoryProviderType repositoryProvider= GetRepositoryProviderType(thisUrl);
+
+ bool isAllowed = GroupPolicyRepositoryEnforcement.IsGroupPolicyEnabled() ? GroupPolicyRepositoryEnforcement.IsRepositoryAllowed(thisUrl) : true;
+
PSRepositoryInfo currentRepoItem = new PSRepositoryInfo(node.Attribute("Name").Value,
thisUrl,
Int32.Parse(node.Attribute("Priority").Value),
Boolean.Parse(node.Attribute("Trusted").Value),
thisCredentialInfo,
- (PSRepositoryInfo.APIVersion)Enum.Parse(typeof(PSRepositoryInfo.APIVersion), node.Attribute("APIVersion").Value, ignoreCase: true));
+ (PSRepositoryInfo.APIVersion)Enum.Parse(typeof(PSRepositoryInfo.APIVersion), node.Attribute("APIVersion").Value, ignoreCase: true),
+ isAllowed);
foundRepos.Add(currentRepoItem);
}
diff --git a/src/code/ServerFactory.cs b/src/code/ServerFactory.cs
index 5127346ab..10c43b1a3 100644
--- a/src/code/ServerFactory.cs
+++ b/src/code/ServerFactory.cs
@@ -1,11 +1,10 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
-using Microsoft.PowerShell.PSResourceGet.UtilClasses;
using System.Collections;
using System.Management.Automation;
-using System.Management.Automation.Runspaces;
using System.Net;
+using Microsoft.PowerShell.PSResourceGet.UtilClasses;
namespace Microsoft.PowerShell.PSResourceGet.Cmdlets
{
diff --git a/test/GroupPolicyEnforcement.Tests.ps1 b/test/GroupPolicyEnforcement.Tests.ps1
new file mode 100644
index 000000000..2353056e2
--- /dev/null
+++ b/test/GroupPolicyEnforcement.Tests.ps1
@@ -0,0 +1,104 @@
+# Copyright (c) Microsoft Corporation.
+# Copyright (c) Microsoft Corporation.
+# Licensed under the MIT License.
+
+$checkIfWindows = [System.Environment]::OSVersion.Platform -eq 'Win32NT'
+
+# Add Pester test to check the API for GroupPolicyEnforcement
+Describe 'GroupPolicyEnforcement API Tests' -Tags 'CI' {
+
+ It 'IsGroupPolicyEnabled should return the correct policy enforcement status' -Skip:(-not $checkIfWindows) {
+ $actualStatus = [Microsoft.PowerShell.PSResourceGet.Cmdlets.GroupPolicyRepositoryEnforcement]::IsGroupPolicyEnabled()
+ $actualStatus | Should -BeFalse
+ }
+
+ It 'IsGroupPolicyEnabled should return false on non-windows platform' -Skip:$checkIfWindows {
+ [Microsoft.PowerShell.PSResourceGet.Cmdlets.GroupPolicyRepositoryEnforcement]::IsGroupPolicyEnabled() | Should -BeFalse
+ }
+
+ It 'GetAllowedRepositoryURIs return null if Group Policy is not enabled' -Skip:(-not $checkIfWindows) {
+ [Microsoft.PowerShell.PSResourceGet.Cmdlets.GroupPolicyRepositoryEnforcement]::GetAllowedRepositoryURIs() | Should -BeNullOrEmpty
+
+ try {
+ [Microsoft.PowerShell.PSResourceGet.UtilClasses.InternalHooks]::SetTestHook('EnableGPRegistryHook', $true)
+ [Microsoft.PowerShell.PSResourceGet.UtilClasses.InternalHooks]::SetTestHook('GPEnabledStatus', $true)
+ [Microsoft.PowerShell.PSResourceGet.UtilClasses.InternalHooks]::SetTestHook('AllowedUri', "https://www.example.com/")
+
+ $allowedReps = [Microsoft.PowerShell.PSResourceGet.Cmdlets.GroupPolicyRepositoryEnforcement]::GetAllowedRepositoryURIs()
+ $allowedReps.AbsoluteUri | Should -Be @("https://www.example.com/")
+ }
+ finally {
+ [Microsoft.PowerShell.PSResourceGet.UtilClasses.InternalHooks]::SetTestHook('EnableGPRegistryHook', $false)
+ [Microsoft.PowerShell.PSResourceGet.UtilClasses.InternalHooks]::SetTestHook('GPEnabledStatus', $false)
+ [Microsoft.PowerShell.PSResourceGet.UtilClasses.InternalHooks]::SetTestHook('AllowedUri', $null)
+ }
+ }
+}
+
+Describe 'GroupPolicyEnforcement Cmdlet Tests' -Tags 'CI' {
+ BeforeEach {
+ [Microsoft.PowerShell.PSResourceGet.UtilClasses.InternalHooks]::SetTestHook('EnableGPRegistryHook', $true)
+ [Microsoft.PowerShell.PSResourceGet.UtilClasses.InternalHooks]::SetTestHook('GPEnabledStatus', $true)
+ [Microsoft.PowerShell.PSResourceGet.UtilClasses.InternalHooks]::SetTestHook('AllowedUri', "https://www.example.com/")
+ }
+
+ AfterEach {
+ [Microsoft.PowerShell.PSResourceGet.UtilClasses.InternalHooks]::SetTestHook('EnableGPRegistryHook', $false)
+ [Microsoft.PowerShell.PSResourceGet.UtilClasses.InternalHooks]::SetTestHook('GPEnabledStatus', $false)
+ [Microsoft.PowerShell.PSResourceGet.UtilClasses.InternalHooks]::SetTestHook('AllowedUri', $null)
+ }
+
+ It 'Get-PSResourceRepository lists the allowed repository' -Skip:(-not $checkIfWindows) {
+ try {
+ Register-PSResourceRepository -Name 'Example' -Uri 'https://www.example.com/'
+ $psrep = Get-PSResourceRepository -Name 'Example'
+ $psrep | Should -Not -BeNullOrEmpty
+ $psrep.IsAllowedByPolicy | Should -BeTrue
+ }
+ finally {
+ Unregister-PSResourceRepository -Name 'Example'
+ }
+ }
+
+ It 'Find-PSResource is blocked by policy' -Skip:(-not $checkIfWindows) {
+ try {
+ Register-PSResourceRepository -Name 'Example' -Uri 'https://www.example.com/' -ApiVersion 'v3'
+ { Find-PSResource -Repository PSGallery -Name 'Az.Accounts' -ErrorAction Stop } | Should -Throw "Repository 'PSGallery' is not allowed by Group Policy."
+
+ # Allow PSGallery and it should not fail
+ [Microsoft.PowerShell.PSResourceGet.UtilClasses.InternalHooks]::SetTestHook('AllowedUri', " https://www.powershellgallery.com/api/v2")
+ { Find-PSResource -Repository PSGallery -Name 'Az.Accounts' -ErrorAction Stop } | Should -Not -Throw
+ }
+ finally {
+ Unregister-PSResourceRepository -Name 'Example'
+ }
+ }
+
+ It 'Install-PSResource is blocked by policy' -Skip:(-not $checkIfWindows) {
+ try {
+ Register-PSResourceRepository -Name 'Example' -Uri 'https://www.example.com/' -ApiVersion 'v3'
+ { Install-PSResource -Repository PSGallery -Name 'Az.Accounts' -ErrorAction Stop } | Should -Throw "Repository 'PSGallery' is not allowed by Group Policy."
+
+ # Allow PSGallery and it should not fail
+ [Microsoft.PowerShell.PSResourceGet.UtilClasses.InternalHooks]::SetTestHook('AllowedUri', " https://www.powershellgallery.com/api/v2")
+ { Install-PSResource -Repository PSGallery -Name 'Az.Accounts' -ErrorAction Stop -TrustRepository} | Should -Not -Throw
+ }
+ finally {
+ Unregister-PSResourceRepository -Name 'Example'
+ }
+ }
+
+ It 'Save-PSResource is blocked by policy' -Skip:(-not $checkIfWindows) {
+ try {
+ Register-PSResourceRepository -Name 'Example' -Uri 'https://www.example.com/' -ApiVersion 'v3'
+ { Save-PSResource -Repository PSGallery -Name 'Az.Accounts' -ErrorAction Stop } | Should -Throw "Repository 'PSGallery' is not allowed by Group Policy."
+
+ # Allow PSGallery and it should not fail
+ [Microsoft.PowerShell.PSResourceGet.UtilClasses.InternalHooks]::SetTestHook('AllowedUri', " https://www.powershellgallery.com/api/v2")
+ { Save-PSResource -Repository PSGallery -Name 'Az.Accounts' -ErrorAction Stop -TrustRepository} | Should -Not -Throw
+ }
+ finally {
+ Unregister-PSResourceRepository -Name 'Example'
+ }
+ }
+}