From 4f225d2c407984db32ed55dc17918570de5463ca Mon Sep 17 00:00:00 2001 From: AmirMS <104940545+AmelBawa-msft@users.noreply.github.com> Date: Mon, 3 Nov 2025 10:14:56 -0800 Subject: [PATCH 1/4] Moved WindowsCapability and PowerPlanSetting --- .../Microsoft.Windows.Settings.psd1 | 8 +- .../Microsoft.Windows.Settings.psm1 | 271 +++++++++++++++++- 2 files changed, 262 insertions(+), 17 deletions(-) diff --git a/resources/Microsoft.Windows.Settings/Microsoft.Windows.Settings.psd1 b/resources/Microsoft.Windows.Settings/Microsoft.Windows.Settings.psd1 index 2bc6d9d4..b528e529 100644 --- a/resources/Microsoft.Windows.Settings/Microsoft.Windows.Settings.psd1 +++ b/resources/Microsoft.Windows.Settings/Microsoft.Windows.Settings.psd1 @@ -8,13 +8,17 @@ Description = 'DSC Resource for Windows Settings' PowerShellVersion = '7.2' DscResourcesToExport = @( - 'WindowsSettings' + 'WindowsSettings', + 'WindowsCapability', + 'PowerPlanSetting' ) PrivateData = @{ PSData = @{ # Tags applied to this module. These help with module discovery in online galleries. Tags = @( - 'PSDscResource_WindowsSettings' + 'PSDscResource_WindowsSettings', + 'PSDscResource_WindowsCapability', + 'PSDscResource_PowerPlanSetting' ) # Prerelease string of this module diff --git a/resources/Microsoft.Windows.Settings/Microsoft.Windows.Settings.psm1 b/resources/Microsoft.Windows.Settings/Microsoft.Windows.Settings.psm1 index 1b4357cb..bb8ca92a 100644 --- a/resources/Microsoft.Windows.Settings/Microsoft.Windows.Settings.psm1 +++ b/resources/Microsoft.Windows.Settings/Microsoft.Windows.Settings.psm1 @@ -4,14 +4,37 @@ $ErrorActionPreference = 'Stop' Set-StrictMode -Version Latest +enum Ensure { + Absent + Present +} + +enum PowerPlanSettingName { + DisplayTimeout + SleepTimeout +} + +enum PowerSource { + # AC + PluggedIn + # DC + Battery + All +} + if ([string]::IsNullOrEmpty($env:TestRegistryPath)) { $global:ExplorerRegistryPath = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced\' $global:PersonalizeRegistryPath = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize\' $global:AppModelUnlockRegistryPath = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock\' + $global:SearchRegistryPath = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Search\' + $global:UACRegistryPath = 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Policies\System\' + $global:RemoteDesktopRegistryPath = 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server' + $global:LongPathsRegistryPath = 'HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem\' } else { - $global:ExplorerRegistryPath = $global:PersonalizeRegistryPath = $global:AppModelUnlockRegistryPath = $env:TestRegistryPath + $global:ExplorerRegistryPath = $global:PersonalizeRegistryPath = $global:AppModelUnlockRegistryPath = $global:SearchRegistryPath = $global:UACRegistryPath = $global:RemoteDesktopRegistryPath = $global:LongPathsRegistryPath = $env:TestRegistryPath } + [DSCResource()] class WindowsSettings { # Key required. Do not set. @@ -62,20 +85,20 @@ class WindowsSettings { # Set TaskbarAlignment if (!$this.TestTaskbarAlignment($currentState)) { - $desiredAlignment = $this.TaskbarAlignment -eq "Left" ? 0 : 1 + $desiredAlignment = $this.TaskbarAlignment -eq 'Left' ? 0 : 1 Set-ItemProperty -Path $global:ExplorerRegistryPath -Name $this.TaskbarAl -Value $desiredAlignment } # Set ColorMode $colorModeChanged = $false if (!$this.TestAppColorMode($currentState)) { - $desiredColorMode = $this.AppColorMode -eq "Dark" ? 0 : 1 + $desiredColorMode = $this.AppColorMode -eq 'Dark' ? 0 : 1 Set-ItemProperty -Path $global:PersonalizeRegistryPath -Name $this.AppsUseLightTheme -Value $desiredColorMode $colorModeChanged = $true } if (!$this.TestSystemColorMode($currentState)) { - $desiredColorMode = $this.SystemColorMode -eq "Dark" ? 0 : 1 + $desiredColorMode = $this.SystemColorMode -eq 'Dark' ? 0 : 1 Set-ItemProperty -Path $global:PersonalizeRegistryPath -Name $this.SystemUsesLightTheme -Value $desiredColorMode $colorModeChanged = $true } @@ -108,37 +131,37 @@ class WindowsSettings { [string] GetTaskbarAlignment() { if (-not(DoesRegistryKeyPropertyExist -Path $global:ExplorerRegistryPath -Name $this.TaskbarAl)) { - return "Center" + return 'Center' } $value = [int](Get-ItemPropertyValue -Path $global:ExplorerRegistryPath -Name $this.TaskbarAl) - return $value -eq 0 ? "Left" : "Center" + return $value -eq 0 ? 'Left' : 'Center' } [string] GetAppColorMode() { if (-not(DoesRegistryKeyPropertyExist -Path $global:PersonalizeRegistryPath -Name $this.AppsUseLightTheme)) { - return "Unknown" + return 'Unknown' } $appsUseLightModeValue = Get-ItemPropertyValue -Path $global:PersonalizeRegistryPath -Name $this.AppsUseLightTheme if ($appsUseLightModeValue -eq 0) { - return "Dark" + return 'Dark' } - return "Light" + return 'Light' } [string] GetSystemColorMode() { if (-not(DoesRegistryKeyPropertyExist -Path $global:PersonalizeRegistryPath -Name $this.SystemUsesLightTheme)) { - return "Unknown" + return 'Unknown' } $systemUsesLightModeValue = Get-ItemPropertyValue -Path $global:PersonalizeRegistryPath -Name $this.SystemUsesLightTheme if ($systemUsesLightModeValue -eq 0) { - return "Dark" + return 'Dark' } - return "Light" + return 'Light' } [bool] IsDeveloperModeEnabled() { @@ -169,6 +192,185 @@ class WindowsSettings { } } +[DSCResource()] +class WindowsCapability { + [DscProperty(Key, Mandatory)] + [string] $Name + + [DscProperty()] + [Ensure] $Ensure = [Ensure]::Present + + [WindowsCapability] Get() { + $currentState = [WindowsCapability]::new() + $windowsCapability = Get-WindowsCapability -Online -Name $this.Name + + # If Name is not set in windowsCapability then the specified capability was not found + if ([System.String]::IsNullOrEmpty($windowsCapability.Name)) { + throw (New-Object -TypeName System.ArgumentException -ArgumentList "$this.Name") + } else { + $currentState.Name = $windowsCapability.Name + + if ($windowsCapability.State -eq 'Installed') { + $currentState.Ensure = [Ensure]::Present + } else { + $currentState.Ensure = [Ensure]::Absent + } + } + + return $currentState + } + + [bool] Test() { + $currentState = $this.Get() + return $currentState.Ensure -eq $this.Ensure + } + + [void] Set() { + # Only make changes if changes are needed + if (-not $this.Test()) { + if ($this.Ensure -eq [Ensure]::Present) { + Add-WindowsCapability -Online -Name $this.Name + } else { + Remove-WindowsCapability -Online -Name $this.Name + } + } + } +} + + +[DSCResource()] +class PowerPlanSetting { + [DscProperty(Key, Mandatory)] + [PowerPlanSettingName]$Name + + [DscProperty(Mandatory)] + [PowerSource]$PowerSource + + [DscProperty(Mandatory)] + [int]$SettingValue + + [DscProperty(NotConfigurable)] + [int] $PluggedInValue + + [DscProperty(NotConfigurable)] + [int] $BatteryValue + + [PowerPlanSetting] Get() { + + function Get-PowerPlanSetting ([PowerPlanSettingName] $SettingName) { + begin { + # If a power plan group policy is set, the power settings cannot be obtained, so temporarily disable it. + $GPReg = Backup-GroupPolicyPowerPlanSetting + if ($GPReg) { + Disable-GroupPolicyPowerPlanSetting + } + } + + process { + $SettingGUID = ($SettingName -eq [PowerPlanSettingName]::DisplayTimeout) ? $DisplayTimeoutSettingGUID : $SleepTimeoutSettingGUID + $PowerPlan = Get-ActivePowerPlan + $planID = $PowerPlan.InstanceId.Split('\')[1] -replace '[{}]' + + $ReturnValue = @{ + PlanGuid = $planID + SettingGuid = $SettingGUID + ACValue = '' + DCValue = '' + } + + foreach ($Power in ('AC', 'DC')) { + $Key = ('{0}Value' -f $Power) + $InstanceId = ('Microsoft:PowerSettingDataIndex\{{{0}}}\{1}\{{{2}}}' -f $planID, $Power, $SettingGUID) + $Instance = (Get-CimInstance -Name root\cimv2\power -Class Win32_PowerSettingDataIndex | Where-Object { $_.InstanceID -eq $InstanceId }) + # error state + if (-not $Instance) { return } + $ReturnValue.$Key = [int]$Instance.SettingIndexValue + } + + return $ReturnValue + } + + end { + if ($GPReg) { + # Restore the group policies + Restore-GroupPolicyPowerPlanSetting -GPRegArray $GPReg + } + } + } + + $Setting = Get-PowerPlanSetting -SettingName $this.Name + $this.PluggedInValue = $Setting.ACValue + $this.BatteryValue = $Setting.DCValue + + $currentState = [PowerPlanSetting]::new() + $currentState.Name = $this.Name + $currentState.SettingValue = $this.SettingValue + $currentState.PluggedInValue = $this.PluggedInValue + $currentState.BatteryValue = $this.BatteryValue + return $currentState + } + + [bool] Test() { + $currentState = $this.Get() + + # User can only specify a single setting value + $pluggedInTest = ($currentState.PluggedInValue -eq $this.SettingValue) + $batteryTest = ($currentState.BatteryValue -eq $this.SettingValue) + + if ($this.PowerSource -eq [PowerSource]::All) { + return ($pluggedInTest -and $batteryTest) + } elseif ($this.PowerSource -eq [PowerSource]::PluggedIn) { + return $pluggedInTest + } else { + return $batteryTest + } + } + + [void] Set() { + function Set-PowerPlanSetting ([PowerPlanSettingName] $PowerPlanSettingName, [PowerSource]$PowerSource, [int]$Value) { + begin { + # If a power plan group policy is set, the power settings cannot be obtained, so temporarily disable it. + $GPReg = Backup-GroupPolicyPowerPlanSetting + if ($GPReg) { + Disable-GroupPolicyPowerPlanSetting + } + } + process { + $SettingGUID = ($PowerPlanSettingName -eq [PowerPlanSettingName]::DisplayTimeout) ? $DisplayTimeoutSettingGUID : $SleepTimeoutSettingGUID + $PowerPlan = Get-ActivePowerPlan + $planID = $PowerPlan.InstanceId.Split('\')[1] -replace '[{}]' + + if ($PowerSource -eq [PowerSource]::All) { + [string[]]$Target = ('AC', 'DC') + } elseif ($PowerSource -eq [PowerSource]::PluggedIn) { + [string[]]$Target = ('AC') + } else { + [string[]]$Target = ('DC') + } + + foreach ($Power in $Target) { + $InstanceId = ('Microsoft:PowerSettingDataIndex\{{{0}}}\{1}\{{{2}}}' -f $planID, $Power, $SettingGUID) + $Instance = Get-CimInstance -Name root\cimv2\power -Class Win32_PowerSettingDataIndex | Where-Object { $_.InstanceID -eq $InstanceId } + # error state + if (-not $Instance) { return } + $Instance | ForEach-Object { $_.SettingIndexValue = $Value } + Set-CimInstance -CimInstance $Instance + } + } + end { + if ($GPReg) { + # Restore the group policies + Restore-GroupPolicyPowerPlanSetting -GPRegArray $GPReg + } + } + } + + if (!$this.Test()) { + Set-PowerPlanSetting -PowerPlanSettingName $this.Name -PowerSource $this.PowerSource -Value $this.SettingValue + } + } +} + function DoesRegistryKeyPropertyExist { param ( [Parameter(Mandatory)] @@ -186,7 +388,7 @@ function DoesRegistryKeyPropertyExist { function SendImmersiveColorSetMessage { param() - Add-Type @" + Add-Type @' using System; using System.Runtime.InteropServices; @@ -196,7 +398,7 @@ public class NativeMethods { IntPtr hWnd, uint Msg, UIntPtr wParam, string lParam, uint fuFlags, uint uTimeout, out UIntPtr lpdwResult); } -"@ +'@ # Constants $HWND_BROADCAST = [IntPtr]0xffff @@ -210,9 +412,48 @@ public class NativeMethods { $HWND_BROADCAST, $WM_SETTINGCHANGE, [UIntPtr]::Zero, - "ImmersiveColorSet", + 'ImmersiveColorSet', $SMTO_ABORTIFHUNG, $timeout, [ref]$result ) } + +function Get-ActivePowerPlan { + Get-CimInstance -Name root\cimv2\power -Class win32_PowerPlan | Where-Object { $_.IsActive } +} + +$SleepTimeoutSettingGUID = '29f6c1db-86da-48c5-9fdb-f2b67b1f44da' +$DisplayTimeoutSettingGUID = '3c0bc021-c8a8-4e07-a973-6b14cbcb2b7e' + +$GroupPolicyPowerPlanRegistryKeyPath = 'HKLM:\SOFTWARE\Policies\Microsoft\Power\PowerSettings' + +function Backup-GroupPolicyPowerPlanSetting { + if (Test-Path $GroupPolicyPowerPlanRegistryKeyPath) { + $Array = @() + Get-ChildItem $GroupPolicyPowerPlanRegistryKeyPath | ForEach-Object { + $Path = $_.PSPath + foreach ($Prop in $_.Property) { + $Array += @{ + Path = $Path + Name = $Prop + Value = Get-ItemPropertyValue -Path $Path -Name $Prop + } + } + } + $Array + } +} + +function Restore-GroupPolicyPowerPlanSetting([HashTable[]]$GPRegArray) { + foreach ($Item in $GPRegArray) { + if (-not (Test-Path $Item.Path)) { + New-Item -Path $Item.Path -ItemType Directory -Force | Out-Null + } + New-ItemProperty @Item -Force | Out-Null + } +} + +function Disable-GroupPolicyPowerPlanSetting { + Remove-Item $GroupPolicyPowerPlanRegistryKeyPath -Recurse -Force | Out-Null +} From 714463d4d6c21086dd6ecdcb0fe3385edaf66011 Mon Sep 17 00:00:00 2001 From: AmirMS <104940545+AmelBawa-msft@users.noreply.github.com> Date: Mon, 3 Nov 2025 10:18:30 -0800 Subject: [PATCH 2/4] Moved AdvancedNetworkSharingSetting --- .../Microsoft.Windows.Settings.psd1 | 6 +- .../Microsoft.Windows.Settings.psm1 | 79 ++++++++++++++++++- 2 files changed, 82 insertions(+), 3 deletions(-) diff --git a/resources/Microsoft.Windows.Settings/Microsoft.Windows.Settings.psd1 b/resources/Microsoft.Windows.Settings/Microsoft.Windows.Settings.psd1 index b528e529..c2a57eb3 100644 --- a/resources/Microsoft.Windows.Settings/Microsoft.Windows.Settings.psd1 +++ b/resources/Microsoft.Windows.Settings/Microsoft.Windows.Settings.psd1 @@ -10,7 +10,8 @@ DscResourcesToExport = @( 'WindowsSettings', 'WindowsCapability', - 'PowerPlanSetting' + 'PowerPlanSetting', + 'AdvancedNetworkSharingSetting' ) PrivateData = @{ PSData = @{ @@ -18,7 +19,8 @@ Tags = @( 'PSDscResource_WindowsSettings', 'PSDscResource_WindowsCapability', - 'PSDscResource_PowerPlanSetting' + 'PSDscResource_PowerPlanSetting', + 'PSDscResource_AdvancedNetworkSharingSetting' ) # Prerelease string of this module diff --git a/resources/Microsoft.Windows.Settings/Microsoft.Windows.Settings.psm1 b/resources/Microsoft.Windows.Settings/Microsoft.Windows.Settings.psm1 index bb8ca92a..e5d51bc6 100644 --- a/resources/Microsoft.Windows.Settings/Microsoft.Windows.Settings.psm1 +++ b/resources/Microsoft.Windows.Settings/Microsoft.Windows.Settings.psm1 @@ -22,6 +22,11 @@ enum PowerSource { All } +enum AdvancedNetworkSharingSettingName { + NetworkDiscovery + FileAndPrinterSharing +} + if ([string]::IsNullOrEmpty($env:TestRegistryPath)) { $global:ExplorerRegistryPath = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced\' $global:PersonalizeRegistryPath = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize\' @@ -237,7 +242,6 @@ class WindowsCapability { } } - [DSCResource()] class PowerPlanSetting { [DscProperty(Key, Mandatory)] @@ -371,6 +375,79 @@ class PowerPlanSetting { } } +[DSCResource()] +class AdvancedNetworkSharingSetting { + [DscProperty(Key, Mandatory)] + [AdvancedNetworkSharingSettingName]$Name + + [DscProperty()] + [string[]]$Profiles = @() + + [DscProperty(NotConfigurable)] + [string[]]$EnabledProfiles + + # Official group names for the firewall rules + hidden [string] $NetworkDiscoveryGroup = '@FirewallAPI.dll,-32752' + hidden [string] $FileAndPrinterSharingGroup = '@FirewallAPI.dll,-28502' + + [AdvancedNetworkSharingSetting] Get() { + $currentState = [AdvancedNetworkSharingSetting]::new() + $currentState.Name = $this.Name + $currentState.Profiles = $this.Profiles + + if ($this.Name -eq [AdvancedNetworkSharingSettingName]::NetworkDiscovery) { + $group = $this.NetworkDiscoveryGroup + } else { + $group = $this.FileAndPrinterSharingGroup + } + + # The group is enabled if all of its sub-rules are enabled and none are disabled. + $this.EnabledProfiles = Get-NetFirewallRule -Group $group | Group-Object Profile | ForEach-Object { + $enabled = ($_.Group.Enabled | Where-Object { $_ -eq 'true' } | Measure-Object).Count + $disabled = ($_.Group.Enabled | Where-Object { $_ -eq 'false' } | Measure-Object).Count + [PSCustomObject]@{ + Profile = $_.Name + Count = $_.Count + Enabled = $enabled + Disabled = $disabled + } + } | Where-Object { ($_.Enabled -gt 0) -and ($_.Disabled -eq 0) -and ($_.Enabled -eq $_.Count) } | Select-Object -Unique -CaseInsensitive -ExpandProperty Profile + + $currentState.EnabledProfiles = $this.EnabledProfiles + + return $currentState + } + + [bool] Test() { + $currentState = $this.Get() + + # Compare-object is case insensitive by default and does not take null arguments + $difference = Compare-Object -ReferenceObject @( $this.Profiles | Select-Object) -DifferenceObject @( $currentState.EnabledProfiles | Select-Object) + return -not $difference + } + + [void] Set() { + if (!$this.Test()) { + if ($this.Name -eq [AdvancedNetworkSharingSettingName]::NetworkDiscovery) { + $group = $this.NetworkDiscoveryGroup + } else { + $group = $this.FileAndPrinterSharingGroup + } + + #Enable, no harm in enabling profiles if they are already enabled + foreach ($profile in $this.Profiles) { + Set-NetFirewallRule -Group $group -Profile $profile -Enabled True + } + + #Disable needed if at least one profile is enabled + $profilesToDisable = Get-NetFirewallRule -Group $group | Where-Object { ($_.Enabled -eq 'True') -and (-not $this.Profiles -contains $_.Profile ) } | Select-Object -Unique -CaseInsensitive -ExpandProperty Profile + foreach ($profile in $profilesToDisable) { + Set-NetFirewallRule -Group $group -Profile $profile -Enabled False + } + } + } +} + function DoesRegistryKeyPropertyExist { param ( [Parameter(Mandatory)] From 9a79eace7efb59313146b7f6e7a4a2316fdb671b Mon Sep 17 00:00:00 2001 From: AmirMS <104940545+AmelBawa-msft@users.noreply.github.com> Date: Mon, 3 Nov 2025 10:19:53 -0800 Subject: [PATCH 3/4] Moved NetConnectionProfile --- .../Microsoft.Windows.Settings.psd1 | 6 ++- .../Microsoft.Windows.Settings.psm1 | 37 +++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/resources/Microsoft.Windows.Settings/Microsoft.Windows.Settings.psd1 b/resources/Microsoft.Windows.Settings/Microsoft.Windows.Settings.psd1 index c2a57eb3..22f0b0a0 100644 --- a/resources/Microsoft.Windows.Settings/Microsoft.Windows.Settings.psd1 +++ b/resources/Microsoft.Windows.Settings/Microsoft.Windows.Settings.psd1 @@ -11,7 +11,8 @@ 'WindowsSettings', 'WindowsCapability', 'PowerPlanSetting', - 'AdvancedNetworkSharingSetting' + 'AdvancedNetworkSharingSetting', + 'NetConnectionProfile' ) PrivateData = @{ PSData = @{ @@ -20,7 +21,8 @@ 'PSDscResource_WindowsSettings', 'PSDscResource_WindowsCapability', 'PSDscResource_PowerPlanSetting', - 'PSDscResource_AdvancedNetworkSharingSetting' + 'PSDscResource_AdvancedNetworkSharingSetting', + 'PSDscResource_NetConnectionProfile' ) # Prerelease string of this module diff --git a/resources/Microsoft.Windows.Settings/Microsoft.Windows.Settings.psm1 b/resources/Microsoft.Windows.Settings/Microsoft.Windows.Settings.psm1 index e5d51bc6..8fbd43f5 100644 --- a/resources/Microsoft.Windows.Settings/Microsoft.Windows.Settings.psm1 +++ b/resources/Microsoft.Windows.Settings/Microsoft.Windows.Settings.psm1 @@ -448,6 +448,43 @@ class AdvancedNetworkSharingSetting { } } +[DSCResource()] +class NetConnectionProfile { + # Key required. Do not set. + [DscProperty(Key)] + [string]$SID + + [DscProperty(Mandatory)] + [string]$InterfaceAlias + + [DscProperty(Mandatory)] + [string]$NetworkCategory + + [NetConnectionProfile] Get() { + $currentState = [NetConnectionProfile]::new() + + $netConnectionProfile = Get-NetConnectionProfile -InterfaceAlias $this.InterfaceAlias -ErrorAction SilentlyContinue + if ($null -eq $netConnectionProfile) { + throw "No network profile found for interface alias '$($this.InterfaceAlias)'" + } + + $currentState.InterfaceAlias = $this.InterfaceAlias + $currentState.NetworkCategory = $netConnectionProfile.NetworkCategory + return $currentState + } + + [bool] Test() { + $currentState = $this.Get() + return $currentState.NetworkCategory -eq $this.NetworkCategory + } + + [void] Set() { + if (-not $this.Test()) { + Set-NetConnectionProfile -InterfaceAlias $this.InterfaceAlias -NetworkCategory $this.NetworkCategory + } + } +} + function DoesRegistryKeyPropertyExist { param ( [Parameter(Mandatory)] From 1ba9645ceed7b57304e5230450c3acf77e4ff157 Mon Sep 17 00:00:00 2001 From: AmirMS <104940545+AmelBawa-msft@users.noreply.github.com> Date: Mon, 3 Nov 2025 10:24:39 -0800 Subject: [PATCH 4/4] Moved FirewallRule --- .../Microsoft.Windows.Settings.psd1 | 6 +- .../Microsoft.Windows.Settings.psm1 | 175 ++++++++++++++++++ 2 files changed, 179 insertions(+), 2 deletions(-) diff --git a/resources/Microsoft.Windows.Settings/Microsoft.Windows.Settings.psd1 b/resources/Microsoft.Windows.Settings/Microsoft.Windows.Settings.psd1 index 22f0b0a0..64a9c5c4 100644 --- a/resources/Microsoft.Windows.Settings/Microsoft.Windows.Settings.psd1 +++ b/resources/Microsoft.Windows.Settings/Microsoft.Windows.Settings.psd1 @@ -12,7 +12,8 @@ 'WindowsCapability', 'PowerPlanSetting', 'AdvancedNetworkSharingSetting', - 'NetConnectionProfile' + 'NetConnectionProfile', + 'FirewallRule' ) PrivateData = @{ PSData = @{ @@ -22,7 +23,8 @@ 'PSDscResource_WindowsCapability', 'PSDscResource_PowerPlanSetting', 'PSDscResource_AdvancedNetworkSharingSetting', - 'PSDscResource_NetConnectionProfile' + 'PSDscResource_NetConnectionProfile', + 'PSDscResource_FirewallRule' ) # Prerelease string of this module diff --git a/resources/Microsoft.Windows.Settings/Microsoft.Windows.Settings.psm1 b/resources/Microsoft.Windows.Settings/Microsoft.Windows.Settings.psm1 index 8fbd43f5..a118227d 100644 --- a/resources/Microsoft.Windows.Settings/Microsoft.Windows.Settings.psm1 +++ b/resources/Microsoft.Windows.Settings/Microsoft.Windows.Settings.psm1 @@ -27,6 +27,17 @@ enum AdvancedNetworkSharingSettingName { FileAndPrinterSharing } +enum Action { + NotConfigured + Allow + Block +} + +enum Direction { + Inbound + Outbound +} + if ([string]::IsNullOrEmpty($env:TestRegistryPath)) { $global:ExplorerRegistryPath = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced\' $global:PersonalizeRegistryPath = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize\' @@ -485,6 +496,150 @@ class NetConnectionProfile { } } +[DSCResource()] +class FirewallRule { + [DscProperty(Key, Mandatory)] + [string]$Name + + [DscProperty()] + [string]$DisplayName + + [DscProperty()] + [Action]$Action + + [DscProperty()] + [string]$Description + + [DscProperty()] + [Direction]$Direction + + [DscProperty()] + [bool]$Enabled + + [DscProperty()] + [Ensure]$Ensure = [Ensure]::Present + + [DscProperty()] + [string[]]$LocalPort + + [DscProperty()] + [string[]]$Profiles + + [DscProperty()] + [string]$Protocol + + [FirewallRule] Get() { + $rule = Get-NetFirewallRule -Name $this.Name -ErrorAction SilentlyContinue + + if (-not $rule) { + return @{ + Ensure = [Ensure]::Absent + Name = $this.Name + } + } + + $properties = $rule | GetNetFirewallPortFilter + return @{ + Ensure = [Ensure]::Present + Name = $rule.Name + DisplayName = $rule.DisplayName + Action = $rule.Action + Description = $rule.Description + Direction = $rule.Direction + Enabled = $rule.Enabled + LocalPort = $properties.LocalPort + # Split the profiles string into an array + Profiles = ($rule.Profile -split ',') | ForEach-Object { $_.Trim() } + Protocol = $properties.Protocol + } + } + + [bool] Test() { + $currentState = $this.Get() + + if ($this.Ensure -eq [Ensure]::Absent) { + return $currentState.Ensure -eq [Ensure]::Absent + } + + # Check each property only if it is specified + if ($this.DisplayName -and ($currentState.DisplayName -ne $this.DisplayName)) { + return $false + } + + if ($currentState.Action -ne $this.Action) { + return $false + } + + if ($this.Description -and ($currentState.Description -ne $this.Description)) { + return $false + } + + if ($currentState.Direction -ne $this.Direction) { + return $false + } + + if ($currentState.Enabled -ne $this.Enabled) { + return $false + } + + if ($this.LocalPort -and (Compare-Object $currentState.LocalPort $this.LocalPort)) { + return $false + } + + if ($this.Profiles -and (Compare-Object $currentState.Profiles $this.Profiles)) { + return $false + } + + if ($this.Protocol -and ($currentState.Protocol -ne $this.Protocol)) { + return $false + } + + return $true + } + + [void] Set() { + # Only make changes if changes are needed + if (-not $this.Test()) { + if ($this.Ensure -eq [Ensure]::Absent) { + Remove-NetFirewallRule -Name $this.Name -ErrorAction SilentlyContinue + } else { + $firewallRule = Get-NetFirewallRule -Name $this.Name + $exists = ($null -ne $firewallRule) + + $params = @{ + # Escape firewall rule name to ensure that wildcard update is not used + Name = ConvertTo-FirewallRuleNameEscapedString -Name $this.Name + DisplayName = $this.DisplayName + Action = $this.Action.ToString() + Description = $this.Description + Direction = $this.Direction.ToString() + Enabled = $this.Enabled.ToString() + Profile = $this.Profiles + Protocol = $this.Protocol + LocalPort = $this.LocalPort + } + + if ($exists) { + <# + If the DisplayName is provided then need to remove it + And change it to NewDisplayName if it is different. + #> + if ($params.ContainsKey('DisplayName')) { + $null = $params.Remove('DisplayName') + if ($this.DisplayName -ne $FirewallRule.DisplayName) { + $null = $params.Add('NewDisplayName', $this.DisplayName) + } + } + + Set-NetFirewallRule @params + } else { + New-NetFirewallRule @params + } + } + } + } +} + function DoesRegistryKeyPropertyExist { param ( [Parameter(Mandatory)] @@ -571,3 +726,23 @@ function Restore-GroupPolicyPowerPlanSetting([HashTable[]]$GPRegArray) { function Disable-GroupPolicyPowerPlanSetting { Remove-Item $GroupPolicyPowerPlanRegistryKeyPath -Recurse -Force | Out-Null } + +# Convert Firewall Rule name to Escape Wildcard Characters. It will append '[', ']' and '*' with a backtick. +function ConvertTo-FirewallRuleNameEscapedString { + [CmdletBinding()] + [OutputType([System.String])] + param + ( + [Parameter(Mandatory = $true)] + $Name + ) + + return $Name.Replace('[', '`[').Replace(']', '`]').Replace('*', '`*') +} + +# Workaround mock issue for Get-NetFirewallPortFilter +function GetNetFirewallPortFilter { + process { + return $_ | Get-NetFirewallPortFilter + } +} \ No newline at end of file