Private/Kinds/Firewall.ps1
|
# The Firewall Kind: what the Windows Firewall of this machine is set to, and what it # lets out. # # Three things a Technician asks of it. Is it on, in the profile that applies right now. # As what did Windows class the network - a machine of a domain that did not recognise # its domain gets the stricter profile, and remote support and its shares stop working # with nothing broken. And which rules are in force that Windows did not bring: what a # Group Policy, an installer or somebody by hand added - a program that cannot reach its # Server on a network in order is sometimes one of them. # # A rule Windows brought is told by its group: Windows names the group of each of its # own rules by a reference into its own files ("@FirewallAPI.dll,-28752"), and the rules # it writes for every Store app of every account carry that account as their owner. # Observed 2026-10-13 on a workstation of a domain: 621 rules in force, 503 of them with # such a group, 60 more with an owner. A rule from a Group Policy is none of Windows' # own, whatever group it has: somebody decided it. # # Read with the cmdlets of the NetSecurity module, from the store that holds what is in # force (ActiveStore): what a Group Policy set and what was set on the machine, merged # the way Windows merges them. Observed 2026-10-13 on a workstation of a domain: all of # it readable without admin rights, the profiles in two seconds, the rules in under one. # What Get-FirewallData hands its Judge. See Private/DataShape.ps1. $script:FirewallDataShape = @{ Service = 'Value' Profiles = 'List' Connections = 'List' ConnectionsRead = 'Answered' Joined = 'Value' Products = 'List' Rules = 'List' RulesRead = 'Answered' RuleCount = 'Value' InboundAllow = 'Value' } function Get-FirewallData { [CmdletBinding()] [OutputType([psobject])] param([hashtable]$Parameters = @{}) $maximum = [int](Get-Parameter $Parameters 'MaximumRules' 300) # The service: without it no rule is in force, and the cmdlets below have no answer. $service = '' try { $service = "$((Get-Service -Name 'mpssvc' -ErrorAction Stop).Status)" } catch { $service = '' } # As Windows says them, undecoded. What is on and what counts is the Judge's call. $profiles = @() try { $profiles = @(Get-NetFirewallProfile -PolicyStore ActiveStore -ErrorAction Stop | ForEach-Object { [pscustomobject]@{ Name = "$($_.Name)" Enabled = "$($_.Enabled)" Inbound = "$($_.DefaultInboundAction)" Outbound = "$($_.DefaultOutboundAction)" LocalRules = "$($_.AllowLocalFirewallRules)" LogBlocked = "$($_.LogBlocked)" } }) } catch { $profiles = @() } # As what Windows classed each connected network: that picks the profile. $connections = @() $connectionsRead = $true try { $connections = @(Get-NetConnectionProfile -ErrorAction Stop | ForEach-Object { [pscustomobject]@{ Alias = "$($_.InterfaceAlias)"; Network = "$($_.Name)"; Category = "$($_.NetworkCategory)" } }) } catch { $connectionsRead = $false } $joined = $null try { $joined = [bool](Get-CimInstance -ClassName Win32_ComputerSystem -ErrorAction Stop).PartOfDomain } catch { $joined = $null } # Another product that registered as the firewall. A server has no such register. $products = @() try { $products = @(Get-CimInstance -Namespace root/SecurityCenter2 -ClassName FirewallProduct -ErrorAction Stop | ForEach-Object { [pscustomobject]@{ DisplayName = "$($_.displayName)" } }) } catch { $products = @() } # The rules in force that Windows did not bring, each with what it is limited to. # The filters are asked for all of them at once, and found by the rule's id. $rules = @() $rulesRead = $true $ruleCount = $null $inboundAllow = $null try { $every = @(Get-NetFirewallRule -PolicyStore ActiveStore -ErrorAction Stop | Where-Object { "$($_.Enabled)" -eq 'True' }) $inboundAllow = @($every | Where-Object { "$($_.Direction)" -eq 'Inbound' -and "$($_.Action)" -eq 'Allow' }).Count $own = @($every | Where-Object { "$($_.PolicyStoreSourceType)" -eq 'GroupPolicy' -or (-not "$($_.Owner)" -and -not "$($_.Group)".StartsWith('@')) }) $ruleCount = $own.Count # What blocks first: that is what a Technician looks for. $found = @($own | Sort-Object @{ Expression = { "$($_.Action)" -ne 'Block' } }, @{ Expression = { "$($_.Direction)" } }, DisplayName | Select-Object -First $maximum) if ($found.Count) { $ports = @{}; $addresses = @{}; $programs = @{}; $services = @{}; $users = @{} $found | Get-NetFirewallPortFilter -ErrorAction SilentlyContinue | ForEach-Object { $ports["$($_.InstanceID)"] = $_ } $found | Get-NetFirewallAddressFilter -ErrorAction SilentlyContinue | ForEach-Object { $addresses["$($_.InstanceID)"] = $_ } $found | Get-NetFirewallApplicationFilter -ErrorAction SilentlyContinue | ForEach-Object { $programs["$($_.InstanceID)"] = $_ } $found | Get-NetFirewallServiceFilter -ErrorAction SilentlyContinue | ForEach-Object { $services["$($_.InstanceID)"] = $_ } $found | Get-NetFirewallSecurityFilter -ErrorAction SilentlyContinue | ForEach-Object { $users["$($_.InstanceID)"] = $_ } $rules = @($found | ForEach-Object { $id = "$($_.InstanceID)" [pscustomobject]@{ Name = "$($_.DisplayName)" Direction = "$($_.Direction)" Action = "$($_.Action)" Profile = "$($_.Profile)" Source = "$($_.PolicyStoreSourceType)" Protocol = "$($ports[$id].Protocol)" LocalPort = (@($ports[$id].LocalPort) | Where-Object { $_ }) -join ', ' RemotePort = (@($ports[$id].RemotePort) | Where-Object { $_ }) -join ', ' RemoteAddress = (@($addresses[$id].RemoteAddress) | Where-Object { $_ }) -join ', ' Program = "$($programs[$id].Program)" Package = "$($programs[$id].Package)" Service = "$($services[$id].Service)" # Whether it is limited to accounts, and not to which: no name is read. LimitedToUser = [bool]("$($users[$id].LocalUser)" -and "$($users[$id].LocalUser)" -ne 'Any') } }) } } catch { $rulesRead = $false } [pscustomobject]@{ PSTypeName = 'Gutcheck.Data.Firewall' Service = $service Profiles = $profiles Connections = $connections ConnectionsRead = $connectionsRead Joined = $joined Products = $products Rules = $rules # An empty list of rules is none only where they were read. RulesRead = $rulesRead # How many there are; Rules holds no more than MaximumRules of them. RuleCount = $ruleCount InboundAllow = $inboundAllow } } function ConvertTo-FirewallFlag { <# .SYNOPSIS What Windows says of a setting of a profile as yes, no, or $null where it says neither ("NotConfigured"). Pure. #> [CmdletBinding()] param([AllowNull()]$Text) switch ("$Text".Trim()) { 'True' { return $true } 'False' { return $false } } $null } function Get-FirewallActive { <# .SYNOPSIS The profiles that apply right now, each with the adapters it applies to. Pure. #> [CmdletBinding()] param([AllowNull()]$Data) $active = [ordered]@{} foreach ($one in @((Get-DataCollection $Data 'Connections') | Where-Object { $_ })) { $name = switch ("$(Get-DataProperty $one 'Category')") { 'DomainAuthenticated' { 'Domain' } 'Private' { 'Private' } 'Public' { 'Public' } default { '' } } if (-not $name) { continue } if (-not $active.Contains($name)) { $active[$name] = @() } $active[$name] += "$(Get-DataProperty $one 'Alias')" } $active } function Get-FirewallProfileText { # The name of a profile as the Report says it. Pure. [CmdletBinding()] param([AllowNull()][string]$Name) switch ("$Name") { 'Domain' { return (Get-Text 'Value.Firewall.Profile.Domain') } 'Private' { return (Get-Text 'Value.Firewall.Profile.Private') } 'Public' { return (Get-Text 'Value.Firewall.Profile.Public') } } "$Name" } function New-FirewallStateFinding { <# .SYNOPSIS Whether the Windows Firewall is on, per profile, and which profile applies. .DESCRIPTION A WARN where the profile that applies is off and no other product registered as the firewall, or where it is on and lets in whatever no rule blocks. A profile that is off and does not apply is said, and no more: it is not what this machine runs on today. #> [CmdletBinding()] param([AllowNull()]$Data) $check = Get-Text 'Check.Firewall.State' $profiles = @((Get-DataCollection $Data 'Profiles') | Where-Object { $_ }) $products = @((Get-DataCollection $Data 'Products') | ForEach-Object { "$(Get-DataProperty $_ 'DisplayName')".Trim() } | Where-Object { $_ } | Select-Object -Unique) $other = '' if ($products.Count) { $other = (Get-Text 'Value.Firewall.Product') -f ($products -join ', ') } if (-not $profiles.Count) { $service = "$(Get-DataProperty $Data 'Service')" if (-not $service -or $service -eq 'Running') { return New-UnavailableFinding -Category Security -Check $check -Hint (Get-Text 'Hint.Firewall.NotRead') } $value = Get-Text 'Value.Firewall.ServiceStopped' if ($other) { return New-Finding -Category Security -Check $check -Severity INFO -Value ('{0} | {1}' -f $value, $other) ` -Meaning (Get-Text 'Meaning.Firewall.OffOther') -Hint (Get-Text 'Hint.Firewall.OffOther') } return New-Finding -Category Security -Check $check -Severity WARN -Value $value ` -Meaning (Get-Text 'Meaning.Firewall.ServiceStopped') -Hint (Get-Text 'Hint.Firewall.Off') } $active = Get-FirewallActive -Data $Data $states = @($profiles | ForEach-Object { $on = ConvertTo-FirewallFlag (Get-DataProperty $_ 'Enabled') $state = if ($null -eq $on) { Get-Text 'Value.Firewall.Unknown' } elseif ($on) { Get-Text 'Value.Firewall.On' } else { Get-Text 'Value.Firewall.Off' } '{0} {1}' -f (Get-FirewallProfileText -Name "$(Get-DataProperty $_ 'Name')"), $state }) $value = @((Get-Text 'Value.Firewall.Profiles') -f ($states -join '; ')) if ($active.Count) { $value += (Get-Text 'Value.Firewall.Active') -f (@($active.Keys | ForEach-Object { '{0} ({1})' -f (Get-FirewallProfileText -Name $_), (@($active[$_]) -join ', ') }) -join ', ') } elseif ((Get-DataProperty $Data 'ConnectionsRead') -ne $false) { $value += Get-Text 'Value.Firewall.ActiveNone' } if ($other) { $value += $other } $off = @($profiles | Where-Object { (ConvertTo-FirewallFlag (Get-DataProperty $_ 'Enabled')) -eq $false } | ForEach-Object { "$(Get-DataProperty $_ 'Name')" }) $offActive = @($off | Where-Object { $active.Contains($_) }) $open = @($profiles | Where-Object { $name = "$(Get-DataProperty $_ 'Name')" $active.Contains($name) -and $off -notcontains $name -and "$(Get-DataProperty $_ 'Inbound')" -eq 'Allow' } | ForEach-Object { Get-FirewallProfileText -Name "$(Get-DataProperty $_ 'Name')" }) if ($open.Count) { $value += (Get-Text 'Value.Firewall.InboundOpen') -f ($open -join ', ') } $reference = @(New-Reference -Section 'Title.Firewall.Profiles') $allowed = ConvertTo-Number (Get-DataProperty $Data 'InboundAllow') if ($null -ne $allowed) { $value += (Get-Text 'Value.Firewall.InboundAllow') -f $allowed } $rules = @((Get-DataCollection $Data 'Rules') | Where-Object { $_ }) if ($rules.Count) { $count = ConvertTo-Number (Get-DataProperty $Data 'RuleCount') if ($null -eq $count -or $count -lt $rules.Count) { $count = $rules.Count } $blocking = @($rules | Where-Object { "$(Get-DataProperty $_ 'Direction')" -eq 'Outbound' -and "$(Get-DataProperty $_ 'Action')" -eq 'Block' }) $value += (Get-Text 'Value.Firewall.OwnRules') -f $count, $blocking.Count if ($count -gt $rules.Count) { $value += (Get-Text 'Value.Firewall.RulesShown') -f $rules.Count } $reference += New-Reference -Section 'Title.Firewall.Rules' } elseif ((Get-DataProperty $Data 'RulesRead') -eq $false) { $value += Get-Text 'Value.Firewall.RulesNotRead' } $severity = 'OK'; $meaning = ''; $hint = '' if ($offActive.Count -and -not $other) { $severity = 'WARN'; $meaning = Get-Text 'Meaning.Firewall.Off'; $hint = Get-Text 'Hint.Firewall.Off' } elseif ($off.Count -and $other) { $severity = 'INFO'; $meaning = Get-Text 'Meaning.Firewall.OffOther'; $hint = Get-Text 'Hint.Firewall.OffOther' } elseif ($open.Count) { $severity = 'WARN'; $meaning = Get-Text 'Meaning.Firewall.InboundOpen'; $hint = Get-Text 'Hint.Firewall.InboundOpen' } elseif ($off.Count) { $severity = 'INFO'; $meaning = Get-Text 'Meaning.Firewall.OffInactive'; $hint = Get-Text 'Hint.Firewall.OffInactive' } New-Finding -Category Security -Check $check -Severity $severity -Value ($value -join ' | ') ` -Meaning $meaning -Hint $hint -Reference $reference } function New-FirewallNetworkFinding { <# .SYNOPSIS As what Windows classed each connected network. Nothing where none is connected or that was not read. .DESCRIPTION A machine of a domain with no network classed as the domain's: right anywhere but in the office, and in the office the reason remote support and the machine's own shares are blocked. Which of the two it is the machine cannot say, so it is said at INFO, with both. #> [CmdletBinding()] param([AllowNull()]$Data) $connections = @((Get-DataCollection $Data 'Connections') | Where-Object { $_ }) if (-not $connections.Count) { return } $named = @($connections | ForEach-Object { $category = switch ("$(Get-DataProperty $_ 'Category')") { 'DomainAuthenticated' { Get-Text 'Value.Firewall.Category.Domain' } 'Private' { Get-Text 'Value.Firewall.Category.Private' } 'Public' { Get-Text 'Value.Firewall.Category.Public' } default { Get-Text 'Value.Firewall.Unknown' } } (Get-Text 'Value.Firewall.Network') -f "$(Get-DataProperty $_ 'Alias')", $category }) $value = (Get-Text 'Value.Firewall.Networks') -f ($named -join '; ') $check = Get-Text 'Check.Firewall.Network' $reference = @(New-Reference -Section 'Title.Firewall.Profiles') $inDomain = @($connections | Where-Object { "$(Get-DataProperty $_ 'Category')" -eq 'DomainAuthenticated' }) if ((Get-DataProperty $Data 'Joined') -eq $true -and -not $inDomain.Count) { return New-Finding -Category Security -Check $check -Severity INFO -Value $value ` -Meaning (Get-Text 'Meaning.Firewall.NotDomain') -Hint (Get-Text 'Hint.Firewall.NotDomain') -Reference $reference } New-Finding -Category Security -Check $check -Severity OK -Value $value -Reference $reference } function ConvertTo-FirewallFinding { [CmdletBinding()] [OutputType([psobject])] param( [AllowNull()]$Data, [hashtable]$Parameters = @{} ) New-FirewallStateFinding -Data $Data New-FirewallNetworkFinding -Data $Data } function ConvertTo-FirewallSection { [CmdletBinding()] [OutputType([psobject])] param([AllowNull()]$Data) $profiles = @((Get-DataCollection $Data 'Profiles') | Where-Object { $_ }) if ($profiles.Count) { $active = Get-FirewallActive -Data $Data New-Section -Title (Get-Text 'Title.Firewall.Profiles') -Help (Get-Text 'Help.Firewall.Profiles') -Row @( $profiles | ForEach-Object { $name = "$(Get-DataProperty $_ 'Name')" # Where Windows says neither, its own default holds: in is blocked, out is not. $in = if ("$(Get-DataProperty $_ 'Inbound')" -eq 'Allow') { Get-Text 'Value.Firewall.Action.Allow' } else { Get-Text 'Value.Firewall.Action.Block' } $out = if ("$(Get-DataProperty $_ 'Outbound')" -eq 'Block') { Get-Text 'Value.Firewall.Action.Block' } else { Get-Text 'Value.Firewall.Action.Allow' } $applies = Get-Text 'Value.Firewall.NotApplied' if ($active.Contains($name)) { $applies = @($active[$name]) -join ', ' } $local = ConvertTo-FirewallFlag (Get-DataProperty $_ 'LocalRules') if ($null -eq $local) { $local = $true } $row = [ordered]@{} $row[(Get-Text 'Column.Firewall.Profile')] = Get-FirewallProfileText -Name $name $row[(Get-Text 'Column.Firewall.AppliesTo')] = $applies $row[(Get-Text 'Column.Firewall.Enabled')] = Get-TriStateText (ConvertTo-FirewallFlag (Get-DataProperty $_ 'Enabled')) $row[(Get-Text 'Column.Firewall.Inbound')] = $in $row[(Get-Text 'Column.Firewall.Outbound')] = $out $row[(Get-Text 'Column.Firewall.LocalRules')] = Get-TriStateText $local $row[(Get-Text 'Column.Firewall.LogBlocked')] = Get-TriStateText ([bool](ConvertTo-FirewallFlag (Get-DataProperty $_ 'LogBlocked'))) [pscustomobject]$row } ) } $rules = @((Get-DataCollection $Data 'Rules') | Where-Object { $_ }) if ($rules.Count) { $any = Get-Text 'Value.Firewall.Any' New-Section -Title (Get-Text 'Title.Firewall.Rules') -Help (Get-Text 'Help.Firewall.Rules') -Row @( $rules | ForEach-Object { $source = switch ("$(Get-DataProperty $_ 'Source')") { 'Local' { Get-Text 'Value.Firewall.Source.Local' } 'GroupPolicy' { Get-Text 'Value.Firewall.Source.GroupPolicy' } default { (Get-Text 'Value.Firewall.Source.Other') -f "$(Get-DataProperty $_ 'Source')" } } $in = @("$(Get-DataProperty $_ 'Profile')" -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ } | ForEach-Object { if ($_ -eq 'Any') { $any } else { Get-FirewallProfileText -Name $_ } }) -join ', ' $limits = @() $program = "$(Get-DataProperty $_ 'Program')".Trim() $service = "$(Get-DataProperty $_ 'Service')".Trim() $package = "$(Get-DataProperty $_ 'Package')".Trim() if ($program -and $program -ne 'Any') { $limits += (Get-Text 'Value.Firewall.Limit.Program') -f $program } if ($service -and $service -ne 'Any') { $limits += (Get-Text 'Value.Firewall.Limit.Service') -f $service } if ($package -and $package -ne 'Any') { $limits += Get-Text 'Value.Firewall.Limit.Package' } if ([bool](Get-DataProperty $_ 'LimitedToUser')) { $limits += Get-Text 'Value.Firewall.Limit.User' } if (-not $limits.Count) { $limits += Get-Text 'Value.Firewall.Limit.None' } $all = { param($Text) if (-not "$Text" -or "$Text" -eq 'Any') { $any } else { "$Text" } } $row = [ordered]@{} $direction = if ("$(Get-DataProperty $_ 'Direction')" -eq 'Outbound') { Get-Text 'Value.Firewall.Direction.Out' } else { Get-Text 'Value.Firewall.Direction.In' } $effect = if ("$(Get-DataProperty $_ 'Action')" -eq 'Block') { Get-Text 'Value.Firewall.Effect.Block' } else { Get-Text 'Value.Firewall.Effect.Allow' } $row[(Get-Text 'Column.Firewall.Rule')] = "$(Get-DataProperty $_ 'Name')" $row[(Get-Text 'Column.Firewall.Direction')] = $direction $row[(Get-Text 'Column.Firewall.Effect')] = $effect $row[(Get-Text 'Column.Firewall.Source')] = $source $row[(Get-Text 'Column.Firewall.Profile')] = $in $row[(Get-Text 'Column.Firewall.Protocol')] = & $all (Get-DataProperty $_ 'Protocol') $row[(Get-Text 'Column.Firewall.LocalPort')] = & $all (Get-DataProperty $_ 'LocalPort') $row[(Get-Text 'Column.Firewall.RemotePort')] = & $all (Get-DataProperty $_ 'RemotePort') $row[(Get-Text 'Column.Firewall.RemoteAddress')] = & $all (Get-DataProperty $_ 'RemoteAddress') $row[(Get-Text 'Column.Firewall.Limit')] = $limits -join ', ' [pscustomobject]$row } ) } } |