diff --git a/.rubocop.yml b/.rubocop.yml index 31e8248..04985fb 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -517,3 +517,7 @@ Style/RedundantArgument: Enabled: false Style/SwapValues: Enabled: false + +# Discard multi-line chains of blocks +Style/MultilineBlockChain: + Enabled: false diff --git a/CHANGELOG.md b/CHANGELOG.md index 35a6957..2dde85b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ All notable changes to this project will be documented in this file. +## Release 1.4.2 (2023-01-22) + +[Full Changelog](https://github.com/webalexeu/puppet-windows_firewall/compare/v1.4.1...v1.4.2) + +**Features** + +- Bug fixes and performance improvements + +**Bugfixes** + +**Known Issues** + ## Release 1.4.1 (2022-12-14) [Full Changelog](https://github.com/webalexeu/puppet-windows_firewall/compare/v1.4.0...v1.4.1) diff --git a/REFERENCE.md b/REFERENCE.md index 6838f18..80b19c9 100644 --- a/REFERENCE.md +++ b/REFERENCE.md @@ -104,16 +104,16 @@ Configures how CRL checking is enforced The following parameters are available in the `windows_firewall_global` type. -* [`name`](#name) -* [`provider`](#provider) +* [`name`](#-windows_firewall_global--name) +* [`provider`](#-windows_firewall_global--provider) -##### `name` +##### `name` namevar Not used (reference only) -##### `provider` +##### `provider` The specific backend to use for this `windows_firewall_global` resource. You will seldom need to specify this --- Puppet will usually discover the appropriate provider for your platform. @@ -128,7 +128,7 @@ The following properties are available in the `windows_firewall_group` type. ##### `enabled` -Valid values: ``true``, ``false`` +Valid values: `true`, `false` Whether the rule group is enabled (`true` or `false`) @@ -138,16 +138,16 @@ Default value: `true` The following parameters are available in the `windows_firewall_group` type. -* [`name`](#name) -* [`provider`](#provider) +* [`name`](#-windows_firewall_group--name) +* [`provider`](#-windows_firewall_group--provider) -##### `name` +##### `name` namevar Name of the rule group to enable/disable -##### `provider` +##### `provider` The specific backend to use for this `windows_firewall_group` resource. You will seldom need to specify this --- Puppet will usually discover the appropriate provider for your platform. @@ -176,7 +176,7 @@ Specifies the localized, user-facing name of the firewall rule being created ##### `enabled` -Valid values: ``true``, ``false`` +Valid values: `true`, `false` This parameter specifies that the rule object is administratively enabled or administratively disabled (`true` or `false`) @@ -276,16 +276,16 @@ Default value: `any` The following parameters are available in the `windows_firewall_ipsec_rule` type. -* [`name`](#name) -* [`provider`](#provider) +* [`name`](#-windows_firewall_ipsec_rule--name) +* [`provider`](#-windows_firewall_ipsec_rule--provider) -##### `name` +##### `name` namevar Name of this rule -##### `provider` +##### `provider` The specific backend to use for this `windows_firewall_ipsec_rule` resource. You will seldom need to specify this --- Puppet will usually discover the appropriate provider for your platform. @@ -348,7 +348,7 @@ Allow remote management of Windows Firewall ##### `state` -Valid values: `on`, `off`, ``true``, ``false`` +Valid values: `on`, `off`, `true`, `false` State of this firewall profile @@ -362,16 +362,16 @@ Control stateful unicast response to multicast The following parameters are available in the `windows_firewall_profile` type. -* [`name`](#name) -* [`provider`](#provider) +* [`name`](#-windows_firewall_profile--name) +* [`provider`](#-windows_firewall_profile--provider) -##### `name` +##### `name` namevar Name of the profile to work on -##### `provider` +##### `provider` The specific backend to use for this `windows_firewall_profile` resource. You will seldom need to specify this --- Puppet will usually discover the appropriate provider for your platform. @@ -428,7 +428,7 @@ Default value: `block` ##### `enabled` -Valid values: ``true``, ``false`` +Valid values: `true`, `false` Whether the rule is enabled (`true` or `false`) @@ -499,10 +499,12 @@ Default value: `any` ##### `protocol` -Valid values: `tcp`, `udp`, `icmpv4`, `icmpv6`, `/^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/` +Valid values: `any`, `tcp`, `udp`, `icmpv4`, `icmpv6`, `/^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/` the protocol the rule targets +Default value: `any` + ##### `remote_address` the remote IP the rule targets (hostname not allowed), use an array to pass more then one @@ -537,16 +539,16 @@ Default value: `any` The following parameters are available in the `windows_firewall_rule` type. -* [`name`](#name) -* [`provider`](#provider) +* [`name`](#-windows_firewall_rule--name) +* [`provider`](#-windows_firewall_rule--provider) -##### `name` +##### `name` namevar Name of this rule -##### `provider` +##### `provider` The specific backend to use for this `windows_firewall_rule` resource. You will seldom need to specify this --- Puppet will usually discover the appropriate provider for your platform. diff --git a/lib/puppet/provider/windows_firewall_global/ruby.rb b/lib/puppet/provider/windows_firewall_global/ruby.rb index 99e43b2..dd6002a 100644 --- a/lib/puppet/provider/windows_firewall_global/ruby.rb +++ b/lib/puppet/provider/windows_firewall_global/ruby.rb @@ -1,16 +1,16 @@ require 'puppet_x' require_relative '../../../puppet_x/windows_firewall' -Puppet::Type.type(:windows_firewall_global).provide(:windows_firewall_global, :parent => Puppet::Provider) do - confine :osfamily => :windows +Puppet::Type.type(:windows_firewall_global).provide(:windows_firewall_global, parent: Puppet::Provider) do + confine osfamily: :windows mk_resource_methods desc 'Windows Firewall global settings' - commands :cmd => 'netsh' + commands cmd: 'netsh' def self.prefetch(resources) instances.each do |prov| - if resource = resources[prov.name] + if (resource = resources[prov.name]) resource.provider = prov end end @@ -28,7 +28,7 @@ def create; end def destroy; end def self.instances - PuppetX::WindowsFirewall.globals(command(:cmd)).collect { |hash| new(hash) } + PuppetX::WindowsFirewall.globals(command(:cmd)).map { |hash| new(hash) } end def flush @@ -41,9 +41,8 @@ def flush :boottimerulecategory, :firewallrulecategory, :stealthrulecategory, - :consecrulecategory - ].include?(property.name) - }.each { |property| + :consecrulecategory].include?(property.name) + }.each do |property| property_name = PuppetX::WindowsFirewall.global_argument_lookup(property.name) property_value = property.value.instance_of?(Array) ? property.value.join(',') : property.value @@ -53,7 +52,6 @@ def flush cmd = "#{command(:cmd)} advfirewall set global #{arg}" output = execute(cmd).to_s Puppet.debug("...#{output}") - } + end end - end diff --git a/lib/puppet/provider/windows_firewall_group/ruby.rb b/lib/puppet/provider/windows_firewall_group/ruby.rb index cbdb3a7..99258ed 100644 --- a/lib/puppet/provider/windows_firewall_group/ruby.rb +++ b/lib/puppet/provider/windows_firewall_group/ruby.rb @@ -1,16 +1,16 @@ require 'puppet_x' require_relative '../../../puppet_x/windows_firewall' -Puppet::Type.type(:windows_firewall_group).provide(:windows_firewall_group, :parent => Puppet::Provider) do - confine :osfamily => :windows +Puppet::Type.type(:windows_firewall_group).provide(:windows_firewall_group, parent: Puppet::Provider) do + confine osfamily: :windows mk_resource_methods desc 'Windows Firewall group' - commands :cmd => 'netsh' + commands cmd: 'netsh' def self.prefetch(resources) instances.each do |prov| - if resource = resources[prov.name] + if (resource = resources[prov.name]) resource.provider = prov end end @@ -28,7 +28,7 @@ def create; end def destroy; end def self.instances - PuppetX::WindowsFirewall.groups.collect { |hash| new(hash) } + PuppetX::WindowsFirewall.groups.map { |hash| new(hash) } end def flush @@ -37,12 +37,11 @@ def flush # to inspect @resource instead # careful its a label not a boolean... - netsh_enabled = (@resource[:enabled] == :true)? 'yes': 'no' + netsh_enabled = (@resource[:enabled] == :true) ? 'yes' : 'no' Puppet.notice("(windows_firewall) group '#{@resource[:name]}' enabled: #{@resource[:enabled]}") cmd = "#{command(:cmd)} advfirewall firewall set rule group=\"#{@resource[:name]}\" new enable=\"#{netsh_enabled}\"" output = execute(cmd).to_s Puppet.debug("...#{output}") end - end diff --git a/lib/puppet/provider/windows_firewall_ipsec_rule/ruby.rb b/lib/puppet/provider/windows_firewall_ipsec_rule/ruby.rb index c5f68b2..c79b357 100644 --- a/lib/puppet/provider/windows_firewall_ipsec_rule/ruby.rb +++ b/lib/puppet/provider/windows_firewall_ipsec_rule/ruby.rb @@ -1,14 +1,14 @@ require 'puppet_x' require_relative '../../../puppet_x/windows_firewall_ipsec' -Puppet::Type.type(:windows_firewall_ipsec_rule).provide(:windows_firewall_ipsec_rule, :parent => Puppet::Provider) do - confine :osfamily => :windows +Puppet::Type.type(:windows_firewall_ipsec_rule).provide(:windows_firewall_ipsec_rule, parent: Puppet::Provider) do + confine osfamily: :windows mk_resource_methods desc 'Windows Firewall' def self.prefetch(resources) instances.each do |prov| - if resource = resources[prov.name] + if (resource = resources[prov.name]) resource.provider = prov end end @@ -27,16 +27,14 @@ def destroy end def self.instances - PuppetX::WindowsFirewallIPSec.rules.collect { |hash| new(hash) } + PuppetX::WindowsFirewallIPSec.rules.map { |hash| new(hash) } end def flush # Update rule # Only if IS value ensure == SHOULD value ensure # @property_hash contains the IS values (thanks Gary!). For new rules there is no IS, there is only the SHOULD - if @property_hash[:ensure] == @resource[:ensure] - PuppetX::WindowsFirewallIPSec.update_rule @resource - end + return unless @property_hash[:ensure] == @resource[:ensure] + PuppetX::WindowsFirewallIPSec.update_rule @resource end - end diff --git a/lib/puppet/provider/windows_firewall_profile/ruby.rb b/lib/puppet/provider/windows_firewall_profile/ruby.rb index 372c077..6a9984e 100644 --- a/lib/puppet/provider/windows_firewall_profile/ruby.rb +++ b/lib/puppet/provider/windows_firewall_profile/ruby.rb @@ -1,16 +1,16 @@ require 'puppet_x' require_relative '../../../puppet_x/windows_firewall' -Puppet::Type.type(:windows_firewall_profile).provide(:windows_firewall_profile, :parent => Puppet::Provider) do - confine :osfamily => :windows +Puppet::Type.type(:windows_firewall_profile).provide(:windows_firewall_profile, parent: Puppet::Provider) do + confine osfamily: :windows mk_resource_methods desc 'Windows Firewall profile' - commands :cmd => 'netsh' + commands cmd: 'netsh' def self.prefetch(resources) instances.each do |prov| - if resource = resources[prov.name] + if (resource = resources[prov.name]) resource.provider = prov end end @@ -27,16 +27,15 @@ def create; end # all work done in `flush()` method def destroy; end - def self.instances - PuppetX::WindowsFirewall.profiles(command(:cmd)).collect { |hash| new(hash) } + PuppetX::WindowsFirewall.profiles(command(:cmd)).map { |hash| new(hash) } end def flush # @property_hash contains the `IS` values (thanks Gary!)... For new rules there is no `IS`, there is only the # `SHOULD`. The setter methods from `mk_resource_methods` (or manually created) won't be called either. You have # to inspect @resource instead - @resource.properties.each { |property| + @resource.properties.each do |property| property_name = PuppetX::WindowsFirewall.profile_argument_lookup(property.name) property_value = property.value @@ -45,7 +44,6 @@ def flush cmd = "#{command(:cmd)} advfirewall set #{@resource[:name]}profile #{arg}" output = execute(cmd).to_s Puppet.debug("...#{output}") - } + end end - end diff --git a/lib/puppet/provider/windows_firewall_rule/ruby.rb b/lib/puppet/provider/windows_firewall_rule/ruby.rb index 792e013..75609d1 100644 --- a/lib/puppet/provider/windows_firewall_rule/ruby.rb +++ b/lib/puppet/provider/windows_firewall_rule/ruby.rb @@ -1,14 +1,14 @@ require 'puppet_x' require_relative '../../../puppet_x/windows_firewall' -Puppet::Type.type(:windows_firewall_rule).provide(:windows_firewall_rule, :parent => Puppet::Provider) do - confine :osfamily => :windows +Puppet::Type.type(:windows_firewall_rule).provide(:windows_firewall_rule, parent: Puppet::Provider) do + confine osfamily: :windows mk_resource_methods desc 'Windows Firewall' def self.prefetch(resources) instances.each do |prov| - if resource = resources[prov.name] + if (resource = resources[prov.name]) resource.provider = prov end end @@ -27,16 +27,14 @@ def destroy end def self.instances - PuppetX::WindowsFirewall.rules.collect { |hash| new(hash) } + PuppetX::WindowsFirewall.rules.map { |hash| new(hash) } end def flush # Update rule # Only if IS value ensure == SHOULD value ensure # @property_hash contains the IS values (thanks Gary!). For new rules there is no IS, there is only the SHOULD - if @property_hash[:ensure] == @resource[:ensure] - PuppetX::WindowsFirewall.update_rule @resource - end + return unless @property_hash[:ensure] == @resource[:ensure] + PuppetX::WindowsFirewall.update_rule @resource end - end diff --git a/lib/puppet/type/windows_firewall_global.rb b/lib/puppet/type/windows_firewall_global.rb index 49900c0..95ce729 100644 --- a/lib/puppet/type/windows_firewall_global.rb +++ b/lib/puppet/type/windows_firewall_global.rb @@ -14,7 +14,7 @@ newproperty(:strongcrlcheck) do desc 'Configures how CRL checking is enforced' validate do |value| - if ! [0,1,2].include? value.to_i + unless [0, 1, 2].include? value.to_i raise('Invalid value, allowed: 0,1,2') end end @@ -25,13 +25,13 @@ validate do |value| value = value.to_i - if ! (value >= 5 && value <= 60) - raise("Invalid value, allowed: 0,1,2") + unless value >= 5 && value <= 60 + raise('Invalid value, allowed: 0,1,2') end end end - newproperty(:defaultexemptions, :array_matching => :all) do + newproperty(:defaultexemptions, array_matching: :all) do desc 'Configures the default IPsec exemptions. Default is to exempt IPv6 neighbordiscovery protocol and DHCP from IPsec' newvalues(:none, :neighbordiscovery, :icmp, :dhcp, :notconfigured) @@ -41,7 +41,6 @@ def insync?(is) # Element-wise comparison - http://ruby-doc.org/core-2.5.1/Array.html (should.map { |e| e.to_s }.sort <=> is.sort) == 0 end - end newproperty(:ipsecthroughnat) do @@ -59,14 +58,14 @@ def insync?(is) newproperty(:authzusergrptransport) do desc 'Authz user group transport' - validate do |value| + validate do |_value| raise('property is read-only') end end newproperty(:authzcomputergrptransport) do desc 'Authz computer transport' - validate do |value| + validate do |_value| raise('property is read-only') end end @@ -96,30 +95,29 @@ def insync?(is) newproperty(:boottimerulecategory) do desc 'Boot time rule category' - validate do |value| + validate do |_value| raise('property is read-only') end end newproperty(:firewallrulecategory) do desc 'Firewall rule category' - validate do |value| + validate do |_value| raise('property is read-only') end end newproperty(:stealthrulecategory) do desc 'Stealth rule category' - validate do |value| + validate do |_value| raise('property is read-only') end end newproperty(:consecrulecategory) do - desc'"con sec rule category' - validate do |value| + desc '"con sec rule category' + validate do |_value| raise('property is read-only') end end - end diff --git a/lib/puppet/type/windows_firewall_group.rb b/lib/puppet/type/windows_firewall_group.rb index 936e433..c373118 100644 --- a/lib/puppet/type/windows_firewall_group.rb +++ b/lib/puppet/type/windows_firewall_group.rb @@ -12,7 +12,7 @@ end newproperty(:enabled) do - desc "Whether the rule group is enabled (`true` or `false`)" + desc 'Whether the rule group is enabled (`true` or `false`)' newvalues(:true, :false) defaultto :true @@ -25,5 +25,4 @@ def insync?(is) is == should end end - end diff --git a/lib/puppet/type/windows_firewall_ipsec_rule.rb b/lib/puppet/type/windows_firewall_ipsec_rule.rb index 02ad8ba..3cdd17d 100644 --- a/lib/puppet/type/windows_firewall_ipsec_rule.rb +++ b/lib/puppet/type/windows_firewall_ipsec_rule.rb @@ -4,7 +4,7 @@ @doc = 'Manage Windows Firewall with Puppet' ensurable do - desc "How to ensure this firewall rule (`present` or `absent`)" + desc 'How to ensure this firewall rule (`present` or `absent`)' defaultto :present defaultvalues @@ -15,7 +15,6 @@ def insync?(is) (is == :present && should == :present) || (is == :absent && should == :absent) end - end # Resource validation @@ -27,7 +26,7 @@ def insync?(is) end newproperty(:enabled) do - desc "This parameter specifies that the rule object is administratively enabled or administratively disabled (`true` or `false`)" + desc 'This parameter specifies that the rule object is administratively enabled or administratively disabled (`true` or `false`)' newvalues(:true, :false) defaultto :true end @@ -36,7 +35,7 @@ def insync?(is) desc 'Specifies the localized, user-facing name of the firewall rule being created' defaultto { @resource[:name] } validate do |value| - unless value.kind_of?(String) + unless value.is_a?(String) raise "Invalid value '#{value}'. Should be a string" end end @@ -46,13 +45,13 @@ def insync?(is) desc 'This parameter provides information about the firewall rule' defaultto '' validate do |value| - unless value.kind_of?(String) + unless value.is_a?(String) raise "Invalid value '#{value}'. Should be a string" end end end - newproperty(:profile, :array_matching=>:all) do + newproperty(:profile, array_matching: :all) do desc 'Specifies one or more profiles to which the rule is assigned' newvalues(:domain, :private, :public, :any) @@ -65,12 +64,12 @@ def insync?(is) newproperty(:display_group) do desc 'This parameter specifies the source string for the DisplayGroup parameter (read-only)' - validate do |value| + validate do |_value| raise 'grouping is readonly: https://social.technet.microsoft.com/Forums/office/en-US/669a8eaf-13d1-4010-b2ac-30c800c4b152/2008r2-firewall-add-rules-to-group-create-new-group' end end - newproperty(:local_address, :array_matching=>:all) do + newproperty(:local_address, array_matching: :all) do desc 'Specifies that network packets with matching IP addresses match this rule (hostname not allowed), use an array to pass more then one' # Checking that old syntax using comma is not used @@ -93,7 +92,7 @@ def insync?(is) defaultto 'any' end - newproperty(:remote_address, :array_matching=>:all) do + newproperty(:remote_address, array_matching: :all) do desc 'Specifies that network packets with matching IP addresses match this rule (hostname not allowed), use an array to pass more then one' # Checking that old syntax using comma is not used @@ -119,14 +118,14 @@ def insync?(is) newproperty(:protocol) do desc 'This parameter specifies the protocol for an IPsec rule' # Also accept 0-255 :/ - newvalues(:tcp, :udp, :icmpv4, :icmpv6, /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/) + newvalues(:tcp, :udp, :icmpv4, :icmpv6, %r{^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$}) isrequired def insync?(is) is.to_s == should.to_s end end - newproperty(:local_port, :array_matching=>:all) do + newproperty(:local_port, array_matching: :all) do desc 'Specifies that network packets with matching IP port numbers match this rule, use an array to pass more then one' # Checking that old syntax using comma is not used @@ -149,7 +148,7 @@ def insync?(is) defaultto 'any' end - newproperty(:remote_port, :array_matching=>:all) do + newproperty(:remote_port, array_matching: :all) do desc 'This parameter value is the second end point of an IPsec rule, use an array to pass more then one' # Checking that old syntax using comma is not used @@ -179,7 +178,7 @@ def insync?(is) defaultto :transport end - newproperty(:interface_type, :array_matching=>:all) do + newproperty(:interface_type, array_matching: :all) do desc 'Specifies that only network connections made through the indicated interface types are subject to the requirements of this rule' newvalues(:any, :wired, :wireless, :remote_access) @@ -222,7 +221,7 @@ def insync?(is) newvalues(:none, :default, :userkerberos) defaultto do - if @resource[:inbound_security] == :require|| @resource[:inbound_security] == :request || @resource[:outbound_security] == :require || @resource[:outbound_security] == :request + if @resource[:inbound_security] == :require || @resource[:inbound_security] == :request || @resource[:outbound_security] == :require || @resource[:outbound_security] == :request :default else :none @@ -234,8 +233,7 @@ def insync?(is) desc 'Name of this rule' isnamevar validate do |value| - raise "it is not allowed to have a rule called 'any'" if value.downcase == 'any' + raise "it is not allowed to have a rule called 'any'" if value.casecmp('any').zero? end end - end diff --git a/lib/puppet/type/windows_firewall_profile.rb b/lib/puppet/type/windows_firewall_profile.rb index 2d62699..2fd7da9 100644 --- a/lib/puppet/type/windows_firewall_profile.rb +++ b/lib/puppet/type/windows_firewall_profile.rb @@ -17,13 +17,13 @@ desc 'State of this firewall profile' newvalues(:on, :off, true, false) munge do |value| - if value == true - munged = :on - elsif value == false - munged = :off - else - munged = value - end + munged = if value == true + :on + elsif value == false + :off + else + value + end munged end @@ -39,15 +39,15 @@ newproperty(:localfirewallrules) do desc 'Merge local firewall rules with Group Policy rules. Valid when configuring a Group Policy store' newvalues(:enable, :disable, :notconfigured) - validate do |value| + validate do |_value| raise("property is read-only because I'm not sure how to read the current value - pls open a ticket with info if you want this") end end newproperty(:localconsecrules) do - desc "Merge local connection security rules with Group Policy rules. Valid when configuring a Group Policy store" + desc 'Merge local connection security rules with Group Policy rules. Valid when configuring a Group Policy store' newvalues(:enable, :disable, :notconfigured) - validate do |value| + validate do |_value| raise("property is read-only because I'm not sure how to read the current value - pls open a ticket with info if you want this") end end diff --git a/lib/puppet/type/windows_firewall_rule.rb b/lib/puppet/type/windows_firewall_rule.rb index 61e700c..7f1ba5a 100644 --- a/lib/puppet/type/windows_firewall_rule.rb +++ b/lib/puppet/type/windows_firewall_rule.rb @@ -4,7 +4,7 @@ @doc = 'Manage Windows Firewall with Puppet' ensurable do - desc "How to ensure this firewall rule (`present` or `absent`)" + desc 'How to ensure this firewall rule (`present` or `absent`)' defaultto :present defaultvalues @@ -15,7 +15,6 @@ def insync?(is) (is == :present && should == :present) || (is == :absent && should == :absent) end - end # Resource validation @@ -29,7 +28,7 @@ def insync?(is) end newproperty(:enabled) do - desc "Whether the rule is enabled (`true` or `false`)" + desc 'Whether the rule is enabled (`true` or `false`)' newvalues(:true, :false) defaultto :true end @@ -38,7 +37,7 @@ def insync?(is) desc 'Display name for this rule' defaultto { @resource[:name] } validate do |value| - unless value.kind_of?(String) + unless value.is_a?(String) raise "Invalid value '#{value}'. Should be a string" end end @@ -48,18 +47,18 @@ def insync?(is) desc 'Description of this rule' defaultto '' validate do |value| - unless value.kind_of?(String) + unless value.is_a?(String) raise "Invalid value '#{value}'. Should be a string" end end end newproperty(:direction) do - desc "Direction the rule applies to (`inbound`/`outbound`)" + desc 'Direction the rule applies to (`inbound`/`outbound`)' newvalues(:inbound, :outbound) isrequired validate do |value| - unless value.kind_of?(String) + unless value.is_a?(String) raise "Invalid value '#{value}'. Should be a string" end unless ['inbound', 'outbound'].include?(value) @@ -68,7 +67,7 @@ def insync?(is) end end - newproperty(:profile, :array_matching=>:all) do + newproperty(:profile, array_matching: :all) do desc 'Which profile(s) this rule belongs to, use an array to pass more then one' newvalues(:domain, :private, :public, :any) @@ -82,12 +81,12 @@ def insync?(is) newproperty(:display_group) do desc 'group that the rule belongs to (read-only)' - validate do |value| + validate do |_value| raise 'grouping is readonly: https://social.technet.microsoft.com/Forums/office/en-US/669a8eaf-13d1-4010-b2ac-30c800c4b152/2008r2-firewall-add-rules-to-group-create-new-group' end end - newproperty(:local_address, :array_matching=>:all) do + newproperty(:local_address, array_matching: :all) do desc 'the local IP the rule targets (hostname not allowed), use an array to pass more then one' # Checking that old syntax using comma is not used @@ -110,7 +109,7 @@ def insync?(is) defaultto 'any' end - newproperty(:remote_address, :array_matching=>:all) do + newproperty(:remote_address, array_matching: :all) do desc 'the remote IP the rule targets (hostname not allowed), use an array to pass more then one' # Checking that old syntax using comma is not used @@ -137,7 +136,7 @@ def insync?(is) desc 'the protocol the rule targets' # Also accept 0-255 :/ - newvalues(:any, :tcp, :udp, :icmpv4, :icmpv6, /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/) + newvalues(:any, :tcp, :udp, :icmpv4, :icmpv6, %r{^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$}) isrequired def insync?(is) is.to_s == should.to_s @@ -163,7 +162,7 @@ def insync?(is) end end - newproperty(:local_port, :array_matching=>:all) do + newproperty(:local_port, array_matching: :all) do desc 'the local port the rule targets, use an array to pass more then one' # Checking that old syntax using comma is not used @@ -185,7 +184,7 @@ def insync?(is) defaultto do # Default is different when icmp_type is used - if @resource[:icmp_type] != :any and !@resource[:icmp_type].nil? + if (@resource[:icmp_type] != :any) && !@resource[:icmp_type].nil? 'rpc' else 'any' @@ -193,7 +192,7 @@ def insync?(is) end end - newproperty(:remote_port, :array_matching=>:all) do + newproperty(:remote_port, array_matching: :all) do desc 'the remote port the rule targets, use an array to pass more then one' # Checking that old syntax using comma is not used @@ -233,13 +232,13 @@ def insync?(is) desc 'Path to program this rule applies to' def insync?(is) - "#{is}".downcase == "#{should}".downcase + is.to_s.casecmp(should.to_s).zero? end defaultto :any end - newproperty(:interface_type, :array_matching=>:all) do + newproperty(:interface_type, array_matching: :all) do desc 'Interface types this rule applies to' newvalues(:any, :wired, :wireless, :remote_access) @@ -254,7 +253,7 @@ def insync?(is) desc 'service names this rule applies to' def insync?(is) - "#{is}".downcase == "#{should}".downcase + is.to_s.casecmp(should.to_s).zero? end defaultto :any @@ -276,7 +275,7 @@ def insync?(is) desc 'Specifies that matching IPsec rules of the indicated computer accounts are created' def insync?(is) - "#{is}".downcase == "#{should}".downcase + is.to_s.casecmp(should.to_s).zero? end defaultto :any @@ -286,7 +285,7 @@ def insync?(is) desc 'Specifies that matching IPsec rules of the indicated user accounts are created' def insync?(is) - "#{is}".downcase == "#{should}".downcase + is.to_s.casecmp(should.to_s).zero? end defaultto :any @@ -296,7 +295,7 @@ def insync?(is) desc 'Specifies that matching IPsec rules of the indicated user accounts are created' def insync?(is) - "#{is}".downcase == "#{should}".downcase + is.to_s.casecmp(should.to_s).zero? end defaultto :any @@ -306,8 +305,7 @@ def insync?(is) desc 'Name of this rule' isnamevar validate do |value| - raise "it is not allowed to have a rule called 'any'" if value.downcase == "any" + raise "it is not allowed to have a rule called 'any'" if value.casecmp('any').zero? end end - end diff --git a/lib/puppet_x/windows_firewall.rb b/lib/puppet_x/windows_firewall.rb index 13e0c95..f42a268 100644 --- a/lib/puppet_x/windows_firewall.rb +++ b/lib/puppet_x/windows_firewall.rb @@ -2,405 +2,374 @@ require 'pp' require 'puppet/util' require 'puppet/util/windows' -module PuppetX - module WindowsFirewall - - MOD_DIR = 'windows_firewall/lib' - SCRIPT_FILE = 'ps-bridge.ps1' - SCRIPT_PATH = File.join('ps/windows_firewall', SCRIPT_FILE) - - # We need to be able to invoke the PS bridge script in both agent and apply - # mode. In agent mode, the file will be found in LIBDIR, in apply mode it will - # be found somewhere under CODEDIR. We need to read from the appropriate dir - # for each mode to work in the most puppety way - def self.resolve_ps_bridge - - case Puppet.run_mode.name - when :user - # AKA `puppet resource` - first scan modules then cache - script = find_ps_bridge_in_modules || find_ps_bridge_in_cache - when :apply - # puppet apply demands local module install... - script = find_ps_bridge_in_modules - when :agent - # agent mode would only look in cache - script = find_ps_bridge_in_cache - else - raise("Don't know how to resolve #{SCRIPT_FILE} for windows_firewall in mode #{Puppet.run_mode.name}") - end - if ! script - raise("windows_firewall unable to find #{SCRIPT_FILE} in expected location") - end +# This module manage Windows Firewall rules +module PuppetX::WindowsFirewall + MOD_DIR = 'windows_firewall/lib'.freeze + SCRIPT_FILE = 'ps-bridge.ps1'.freeze + SCRIPT_PATH = File.join('ps/windows_firewall', SCRIPT_FILE) + + # We need to be able to invoke the PS bridge script in both agent and apply + # mode. In agent mode, the file will be found in LIBDIR, in apply mode it will + # be found somewhere under CODEDIR. We need to read from the appropriate dir + # for each mode to work in the most puppety way + def self.resolve_ps_bridge + case Puppet.run_mode.name + when :user + # AKA `puppet resource` - first scan modules then cache + script = find_ps_bridge_in_modules || find_ps_bridge_in_cache + when :apply + # puppet apply demands local module install... + script = find_ps_bridge_in_modules + when :agent + # agent mode would only look in cache + script = find_ps_bridge_in_cache + else + raise("Don't know how to resolve #{SCRIPT_FILE} for windows_firewall in mode #{Puppet.run_mode.name}") + end - cmd = ['powershell.exe', '-ExecutionPolicy', 'Bypass', '-File', script] - cmd + unless script + raise("windows_firewall unable to find #{SCRIPT_FILE} in expected location") end - def self.find_ps_bridge_in_modules - # 1st priority - environment - check_for_script = File.join( - Puppet.settings[:environmentpath], - Puppet.settings[:environment], - 'modules', - MOD_DIR, - SCRIPT_PATH, - ) - Puppet.debug("Checking for #{SCRIPT_FILE} at #{check_for_script}") - if File.exists? check_for_script - script = check_for_script - else - # 2nd priority - custom module path, then basemodulepath - full_module_path = "#{Puppet.settings[:modulepath]}#{File::PATH_SEPARATOR}#{Puppet.settings[:basemodulepath]}" - full_module_path.split(File::PATH_SEPARATOR).reject do |path_element| - path_element.empty? - end.each do |path_element| - check_for_script = File.join(path_element, MOD_DIR, SCRIPT_PATH) - Puppet.debug("Checking for #{SCRIPT_FILE} at #{check_for_script}") - if File.exists? check_for_script - script = check_for_script - break; - end + cmd = ['powershell.exe', '-ExecutionPolicy', 'Bypass', '-File', script] + cmd + end + + def self.find_ps_bridge_in_modules + # 1st priority - environment + check_for_script = File.join( + Puppet.settings[:environmentpath], + Puppet.settings[:environment], + 'modules', + MOD_DIR, + SCRIPT_PATH, + ) + Puppet.debug("Checking for #{SCRIPT_FILE} at #{check_for_script}") + if File.exist? check_for_script + script = check_for_script + else + # 2nd priority - custom module path, then basemodulepath + full_module_path = "#{Puppet.settings[:modulepath]}#{File::PATH_SEPARATOR}#{Puppet.settings[:basemodulepath]}" + full_module_path.split(File::PATH_SEPARATOR).reject { |path_element| + path_element.empty? + }.each do |path_element| + check_for_script = File.join(path_element, MOD_DIR, SCRIPT_PATH) + Puppet.debug("Checking for #{SCRIPT_FILE} at #{check_for_script}") + if File.exist? check_for_script + script = check_for_script + break end end - - script end - def self.find_ps_bridge_in_cache - check_for_script = File.join(Puppet.settings[:libdir], SCRIPT_PATH) + script + end - Puppet.debug("Checking for #{SCRIPT_FILE} at #{check_for_script}") - script = File.exists?(check_for_script) ? check_for_script : nil - script - end + def self.find_ps_bridge_in_cache + check_for_script = File.join(Puppet.settings[:libdir], SCRIPT_PATH) - # convert a puppet type key name to the argument to use for `netsh` command - def self.global_argument_lookup(key) - { - :keylifetime => 'mainmode mmkeylifetime', - :secmethods => 'mainmode mmsecmethods', - :forcedh => 'mainmode mmforcedh', - :strongcrlcheck => 'ipsec strongcrlcheck', - :saidletimemin => 'ipsec saidletimemin', - :defaultexemptions => 'ipsec defaultexemptions', - :ipsecthroughnat => 'ipsec ipsecthroughnat', - :authzcomputergrp => 'ipsec authzcomputergrp', - :authzusergrp => 'ipsec authzusergrp', - }.fetch(key, key.to_s) - end + Puppet.debug("Checking for #{SCRIPT_FILE} at #{check_for_script}") + script = File.exist?(check_for_script) ? check_for_script : nil + script + end - # convert a puppet type key name to the argument to use for `netsh` command - def self.profile_argument_lookup(key) - { - :localfirewallrules => 'settings localfirewallrules', - :localconsecrules => 'settings localconsecrules', - :inboundusernotification => 'settings inboundusernotification', - :remotemanagement => 'settings remotemanagement', - :unicastresponsetomulticast => 'settings unicastresponsetomulticast', - :logallowedconnections => 'logging allowedconnections', - :logdroppedconnections => 'logging droppedconnections', - :filename => 'logging filename', - :maxfilesize => 'logging maxfilesize', - }.fetch(key, key.to_s) - end + # convert a puppet type key name to the argument to use for `netsh` command + def self.global_argument_lookup(key) + { + keylifetime: 'mainmode mmkeylifetime', + secmethods: 'mainmode mmsecmethods', + forcedh: 'mainmode mmforcedh', + strongcrlcheck: 'ipsec strongcrlcheck', + saidletimemin: 'ipsec saidletimemin', + defaultexemptions: 'ipsec defaultexemptions', + ipsecthroughnat: 'ipsec ipsecthroughnat', + authzcomputergrp: 'ipsec authzcomputergrp', + authzusergrp: 'ipsec authzusergrp', + }.fetch(key, key.to_s) + end - def self.to_ps(key) - { - :enabled => lambda { |x| camel_case(x) }, - :action => lambda { |x| camel_case(x) }, - :direction => lambda { |x| camel_case(x) }, - :description => lambda { |x| x.empty? == true ? "\"#{x}\"" : x }, - :interface_type => lambda { |x| x.map { |e| camel_case(e) }.join(',') }, - :profile => lambda { |x| x.map { |e| camel_case(e) }.join(',') }, - :protocol => lambda { |x| x.to_s.upcase.sub('V', 'v') }, - :icmp_type => lambda { |x| camel_case(x) }, - :edge_traversal_policy => lambda { |x| camel_case(x) }, - :local_port => lambda { |x| x.kind_of?(Array) ? (x.map { |e| camel_case(e) }).join(',') : camel_case(x) }, - :remote_port => lambda { |x| x.kind_of?(Array) ? (x.map { |e| camel_case(e) }).join(',') : camel_case(x) }, - :local_address => lambda { |x| x.kind_of?(Array) ? (x.map { |e| camel_case(e) }).join(',') : camel_case(x) }, - :remote_address => lambda { |x| x.kind_of?(Array) ? (x.map { |e| camel_case(e) }).join(',') : camel_case(x) }, - :program => lambda { |x| x.to_s == 'any' ? x : x.gsub(/\\/, '\\\\') }, - :authentication => lambda { |x| camel_case(x) }, - :encryption => lambda { |x| camel_case(x) }, - :remote_machine => lambda { |x| convert_to_sddl(x) }, - :local_user => lambda { |x| convert_to_sddl(x) }, - :remote_user => lambda { |x| convert_to_sddl(x) }, - }.fetch(key, lambda { |x| x }) - end + # convert a puppet type key name to the argument to use for `netsh` command + def self.profile_argument_lookup(key) + { + localfirewallrules: 'settings localfirewallrules', + localconsecrules: 'settings localconsecrules', + inboundusernotification: 'settings inboundusernotification', + remotemanagement: 'settings remotemanagement', + unicastresponsetomulticast: 'settings unicastresponsetomulticast', + logallowedconnections: 'logging allowedconnections', + logdroppedconnections: 'logging droppedconnections', + filename: 'logging filename', + maxfilesize: 'logging maxfilesize', + }.fetch(key, key.to_s) + end - def self.to_ruby(key) - { - :enabled => lambda { |x| snake_case_sym(x) }, - :action => lambda { |x| snake_case_sym(x) }, - :direction => lambda { |x| snake_case_sym(x) }, - :interface_type => lambda { |x| x.split(',').map{ |e| snake_case_sym(e.strip) } }, - :profile => lambda { |x| x.split(',').map{ |e| snake_case_sym(e.strip) } }, - :protocol => lambda { |x| snake_case_sym(x) }, - :icmp_type => lambda { |x| x ? x.downcase : x }, - :edge_traversal_policy => lambda { |x| snake_case_sym(x) }, - :program => lambda { |x| x.to_s == 'Any' ? x.downcase : x.gsub(/\\\\/, '\\') }, - :remote_port => lambda { |x| x.kind_of?(Array) ? x.map { |e| e.downcase } : x.downcase.split }, - :local_port => lambda { |x| x.kind_of?(Array) ? x.map { |e| e.downcase } : x.downcase.split }, - :remote_address => lambda { |x| x.kind_of?(Array) ? x.map { |e| e.downcase } : x.downcase.split }, - :local_address => lambda { |x| x.kind_of?(Array) ? x.map { |e| e.downcase } : x.downcase.split }, - :authentication => lambda { |x| x.downcase }, - :encryption => lambda { |x| x.downcase }, - :remote_machine => lambda { |x| convert_from_sddl(x) }, - :local_user => lambda { |x| convert_from_sddl(x) }, - :remote_user => lambda { |x| convert_from_sddl(x) }, - :service => lambda { |x| x.downcase }, - }.fetch(key, lambda { |x| x }) - end + def self.to_ps(key) + { + enabled: ->(x) { camel_case(x) }, + action: ->(x) { camel_case(x) }, + direction: ->(x) { camel_case(x) }, + description: ->(x) { (x.empty? == true) ? "\"#{x}\"" : x }, + interface_type: ->(x) { x.map { |e| camel_case(e) }.join(',') }, + profile: ->(x) { x.map { |e| camel_case(e) }.join(',') }, + protocol: ->(x) { x.to_s.upcase.sub('V', 'v') }, + icmp_type: ->(x) { camel_case(x) }, + edge_traversal_policy: ->(x) { camel_case(x) }, + local_port: ->(x) { x.is_a?(Array) ? (x.map { |e| camel_case(e) }).join(',') : camel_case(x) }, + remote_port: ->(x) { x.is_a?(Array) ? (x.map { |e| camel_case(e) }).join(',') : camel_case(x) }, + local_address: ->(x) { x.is_a?(Array) ? (x.map { |e| camel_case(e) }).join(',') : camel_case(x) }, + remote_address: ->(x) { x.is_a?(Array) ? (x.map { |e| camel_case(e) }).join(',') : camel_case(x) }, + program: ->(x) { (x.to_s == 'any') ? x : x.gsub(%r{\\}, '\\\\') }, + authentication: ->(x) { camel_case(x) }, + encryption: ->(x) { camel_case(x) }, + remote_machine: ->(x) { convert_to_sddl(x) }, + local_user: ->(x) { convert_to_sddl(x) }, + remote_user: ->(x) { convert_to_sddl(x) }, + }.fetch(key, ->(x) { x }) + end - # Convert name to SID and structure result as SDDL value - def self.convert_to_sddl_acl(value, ace) - # we need to convert users to sids first - sids = [] - value.split(',').sort.each do |name| - name.strip! - sid = Puppet::Util::Windows::SID.name_to_sid(name) - # If resolution failed, thrown a warning - if sid.nil? - warn("\"#{value}\" does not exist") - else - # Generate structured SSDL ACL - cur_sid = '('+ ace +';;CC;;;' + sid + ')' - end - sids << cur_sid unless cur_sid.nil? - end - sids.sort.join('') - end + def self.to_ruby(key) + { + enabled: ->(x) { snake_case_sym(x) }, + action: ->(x) { snake_case_sym(x) }, + direction: ->(x) { snake_case_sym(x) }, + interface_type: ->(x) { x.split(',').map { |e| snake_case_sym(e.strip) } }, + profile: ->(x) { x.split(',').map { |e| snake_case_sym(e.strip) } }, + protocol: ->(x) { snake_case_sym(x) }, + icmp_type: ->(x) { x ? x.downcase : x }, + edge_traversal_policy: ->(x) { snake_case_sym(x) }, + program: ->(x) { (x.to_s == 'Any') ? x.downcase : x.gsub(%r{\\\\}, '\\') }, + remote_port: ->(x) { x.is_a?(Array) ? x.map { |e| e.downcase } : x.downcase.split }, + local_port: ->(x) { x.is_a?(Array) ? x.map { |e| e.downcase } : x.downcase.split }, + remote_address: ->(x) { x.is_a?(Array) ? x.map { |e| e.downcase } : x.downcase.split }, + local_address: ->(x) { x.is_a?(Array) ? x.map { |e| e.downcase } : x.downcase.split }, + authentication: ->(x) { x.downcase }, + encryption: ->(x) { x.downcase }, + remote_machine: ->(x) { convert_from_sddl(x) }, + local_user: ->(x) { convert_from_sddl(x) }, + remote_user: ->(x) { convert_from_sddl(x) }, + service: ->(x) { x.downcase }, + }.fetch(key, ->(x) { x }) + end - # Convert name to SID and structure result as SDDL value (Only if value is not any) - def self.convert_to_sddl(value) - if value.to_s == 'any' - value + # Convert name to SID and structure result as SDDL value + def self.convert_to_sddl_acl(value, ace) + # we need to convert users to sids first + sids = [] + value.split(',').sort.each do |name| + name.strip! + sid = Puppet::Util::Windows::SID.name_to_sid(name) + # If resolution failed, thrown a warning + if sid.nil? + warn("\"#{value}\" does not exist") else - 'O:LSD:' + (convert_to_sddl_acl(value['allow'], 'A') unless value['allow'].nil?).to_s + (convert_to_sddl_acl(value['block'], 'D') unless value['block'].nil?).to_s + # Generate structured SSDL ACL + cur_sid = '(' + ace + ';;CC;;;' + sid + ')' end + sids << cur_sid unless cur_sid.nil? end + sids.sort.join('') + end - # Parse SDDL value and convert SID to name - def self.convert_from_sddl(value) - if value == 'Any' - # Return value in lowercase - value.downcase! - else - # we need to convert users to sids first - # Delete prefix - value.delete_prefix! 'O:LSD:' - # Change ')(' to ',' to have a proper delimiter - value.gsub! ')(', ',' - # Remove '()' - value.delete! '()' - # Define variables - names = {} - allow = [] - deny = [] - value.split(',').sort.each do |sid| - # ACE is first character - ace = sid.chr.upcase - # Delete prefix on each user - sid.delete_prefix! ace + ';;CC;;;' - sid.strip! - name = Puppet::Util::Windows::SID.sid_to_name(sid) - # If resolution failed, return SID - if name.nil? - cur_name = sid.downcase! - else - cur_name = name.downcase! - end - case ace - when 'A' - allow << cur_name unless cur_name.nil? - when 'D' - deny << cur_name unless cur_name.nil? - end - end - if !allow.empty? - names['allow'] = allow.sort.join(',') - end - if !deny.empty? - names['block'] = deny.sort.join(',') - end - names - end + # Convert name to SID and structure result as SDDL value (Only if value is not any) + def self.convert_to_sddl(value) + if value.to_s == 'any' + value + else + 'O:LSD:' + (convert_to_sddl_acl(value['allow'], 'A') unless value['allow'].nil?).to_s + (convert_to_sddl_acl(value['block'], 'D') unless value['block'].nil?).to_s end + end - # create a normalised key name by: - # 1. lowercasing input - # 2. converting spaces to underscores - # 3. convert to symbol - def self.key_name(input) - input.downcase.gsub(/\s/, '_').to_sym + # Parse SDDL value and convert SID to name + def self.convert_from_sddl(value) + if value == 'Any' + # Return value in lowercase + value.downcase! + else + # we need to convert users to sids first + # Delete prefix + value.delete_prefix! 'O:LSD:' + # Change ')(' to ',' to have a proper delimiter + value.gsub! ')(', ',' + # Remove '()' + value.delete! '()' + # Define variables + names = {} + allow = [] + deny = [] + value.split(',').sort.each do |sid| + # ACE is first character + ace = sid.chr.upcase + # Delete prefix on each user + sid.delete_prefix! ace + ';;CC;;;' + sid.strip! + name = Puppet::Util::Windows::SID.sid_to_name(sid) + # If resolution failed, return SID + cur_name = if name.nil? + sid.downcase! + else + name.downcase! + end + case ace + when 'A' + allow << cur_name unless cur_name.nil? + when 'D' + deny << cur_name unless cur_name.nil? + end + end + unless allow.empty? + names['allow'] = allow.sort.join(',') + end + unless deny.empty? + names['block'] = deny.sort.join(',') + end + names end + end - # Convert input CamelCase to snake_case symbols - def self.snake_case_sym(input) - input.gsub(/([a-z])([A-Z])/, '\1_\2').downcase.to_sym - end + # create a normalised key name by: + # 1. lowercasing input + # 2. converting spaces to underscores + # 3. convert to symbol + def self.key_name(input) + input.downcase.gsub(%r{\s}, '_').to_sym + end - # Convert snake_case input symbol to CamelCase string - def self.camel_case(input) - # https://stackoverflow.com/a/24917606/3441106 - input.to_s.split('_').collect(&:capitalize).join - end + # Convert input CamelCase to snake_case symbols + def self.snake_case_sym(input) + input.gsub(%r{([a-z])([A-Z])}, '\1_\2').downcase.to_sym + end - def self.delete_rule(resource) - Puppet.notice("(windows_firewall) deleting rule '#{resource[:display_name]}'") - out = Puppet::Util::Execution.execute(resolve_ps_bridge + ["delete", resource[:name]]).to_s - Puppet.debug out - end + # Convert snake_case input symbol to CamelCase string + def self.camel_case(input) + # https://stackoverflow.com/a/24917606/3441106 + input.to_s.split('_').map(&:capitalize).join + end - def self.update_rule(resource) - Puppet.notice("(windows_firewall) updating rule '#{resource[:display_name]}'") + def self.delete_rule(resource) + Puppet.notice("(windows_firewall) deleting rule '#{resource[:display_name]}'") + out = Puppet::Util::Execution.execute(resolve_ps_bridge + ['delete', resource[:name]]).to_s + Puppet.debug out + end - # `Name` is mandatory and also a `parameter` not a `property` - args = [ '-Name', resource[:name] ] + def self.update_rule(resource) + Puppet.notice("(windows_firewall) updating rule '#{resource[:display_name]}'") - resource.properties.reject { |property| - [:ensure, :protocol_type, :protocol_code].include?(property.name) || - property.value == :none - }.each { |property| - # All properties start `-` - property_name = "-#{camel_case(property.name)}" - property_value = to_ps(property.name).call(property.value) + # `Name` is mandatory and also a `parameter` not a `property` + args = [ '-Name', resource[:name] ] - # protocol can optionally specify type and code, other properties are set very simply - args << property_name - args << property_value - } - Puppet.debug "Updating firewall rule with args: #{args}" + resource.properties.reject { |property| + [:ensure, :protocol_type, :protocol_code].include?(property.name) || + property.value == :none + }.each do |property| + # All properties start `-` + property_name = "-#{camel_case(property.name)}" + property_value = to_ps(property.name).call(property.value) - out = Puppet::Util::Execution.execute(resolve_ps_bridge + ['update'] + args) - Puppet.debug out + # protocol can optionally specify type and code, other properties are set very simply + args << property_name + args << property_value end + Puppet.debug "Updating firewall rule with args: #{args}" - # Create a new firewall rule using powershell - # @see https://docs.microsoft.com/en-us/powershell/module/netsecurity/new-netfirewallrule?view=win10-ps - def self.create_rule(resource) - Puppet.notice("(windows_firewall) adding rule '#{resource[:display_name]}'") - - # `Name` is mandatory and also a `parameter` not a `property` - args = [ '-Name', resource[:name] ] - - resource.properties.reject { |property| - [:ensure, :protocol_type, :protocol_code].include?(property.name) || - property.value == :none - }.each { |property| - # All properties start `-` - property_name = "-#{camel_case(property.name)}" - property_value = to_ps(property.name).call(property.value) - - # protocol can optionally specify type and code, other properties are set very simply - args << property_name - args << property_value - } - Puppet.debug "Creating firewall rule with args: #{args}" - - out = Puppet::Util::Execution.execute(resolve_ps_bridge + ['create'] + args) - Puppet.debug out - end + out = Puppet::Util::Execution.execute(resolve_ps_bridge + ['update'] + args) + Puppet.debug out + end - def self.rules - Puppet.debug('query all rules') - rules = JSON.parse Puppet::Util::Execution.execute(resolve_ps_bridge + ['show']).to_s - - # Rules is an array of hash as-parsed and hash keys need converted to - # lowercase ruby labels - puppet_rules = rules.map { |e| - Hash[e.map { |k, v| - key = snake_case_sym(k) - [key, to_ruby(key).call(v)] - }].merge({ensure: :present}) - } - Puppet.debug("Parsed rules: #{puppet_rules.size}") - puppet_rules + # Create a new firewall rule using powershell + # @see https://docs.microsoft.com/en-us/powershell/module/netsecurity/new-netfirewallrule?view=win10-ps + def self.create_rule(resource) + Puppet.notice("(windows_firewall) adding rule '#{resource[:display_name]}'") + + # `Name` is mandatory and also a `parameter` not a `property` + args = [ '-Name', resource[:name] ] + + resource.properties.reject { |property| + [:ensure, :protocol_type, :protocol_code].include?(property.name) || + property.value == :none + }.each do |property| + # All properties start `-` + property_name = "-#{camel_case(property.name)}" + property_value = to_ps(property.name).call(property.value) + + # protocol can optionally specify type and code, other properties are set very simply + args << property_name + args << property_value end + Puppet.debug "Creating firewall rule with args: #{args}" - def self.groups - Puppet.debug('query all groups') - # get all individual firewall rules, then create a new hash containing the overall group - # status for each group of rules - g = {} - rules.select { |e| - # we are only interested in firewall rules that provide grouping information so bounce - # anything that doesn't have it from the list - ! e[:display_group].empty? - }.each { |e| - # extract the group information for each rule, use the value of :enabled to - # build up an overall status for the whole group. Dont forget that the - # value is a label :true or :false - to fit with puppet's newtype operator - k = e[:display_group] - current = g.fetch(k, e[:enabled]) - - if current == :true && e[:enabled] == :true - g[k] = :true - else - g[k] = :false - end - - } - - # convert into puppet's preferred hash format which is an array of hashes - # with each hash representing a distinct resource - transformed = g.map { |k, v| - { :name => k, :enabled => v} - } + out = Puppet::Util::Execution.execute(resolve_ps_bridge + ['create'] + args) + Puppet.debug out + end - Puppet.debug("group rules #{transformed}") - transformed + def self.rules + Puppet.debug('query all rules') + rules = JSON.parse Puppet::Util::Execution.execute(resolve_ps_bridge + ['show']).to_s + + # Rules is an array of hash as-parsed and hash keys need converted to + # lowercase ruby labels + puppet_rules = rules.map do |e| + Hash[e.map do |k, v| + key = snake_case_sym(k) + [key, to_ruby(key).call(v)] + end].merge({ ensure: :present }) end + Puppet.debug("Parsed rules: #{puppet_rules.size}") + puppet_rules + end + def self.groups + Puppet.debug('query all groups') + # get all individual firewall rules, then create a new hash containing the overall group + # status for each group of rules + g = {} + rules.reject { |e| + # we are only interested in firewall rules that provide grouping information so bounce + # anything that doesn't have it from the list + e[:display_group].empty? + }.each do |e| + # extract the group information for each rule, use the value of :enabled to + # build up an overall status for the whole group. Dont forget that the + # value is a label :true or :false - to fit with puppet's newtype operator + k = e[:display_group] + current = g.fetch(k, e[:enabled]) + + g[k] = if current == :true && e[:enabled] == :true + :true + else + :false + end + end - # Each rule is se - def self.parse_profile(input) - profile = {} - first_line = true - profile_name = '__error__' - input.split("\n").reject { |line| - line =~ /---/ || line =~ /^\s*$/ - }.each { |line| - if first_line - # take the first word in the line - eg "public profile settings" -> "public" - profile_name = line.split(" ")[0].downcase - first_line = false - else - # nasty hack - "firewall policy" setting contains space and will break our - # logic below. Also the setter in `netsh` to use is `firewallpolicy`. Just fix it... - line = line.sub('Firewall Policy', 'firewallpolicy') - - # split each line at most twice by first glob of whitespace - line_split = line.split(/\s+/, 2) - - if line_split.size == 2 - key = key_name(line_split[0].strip) - - # downcase all values for comparison purposes - value = line_split[1].strip.downcase - - profile[key] = value - end - end - } - - # if we see the rule then it must exist... - profile[:name] = profile_name - - Puppet.debug "Parsed windows firewall profile: #{profile}" - profile + # convert into puppet's preferred hash format which is an array of hashes + # with each hash representing a distinct resource + transformed = g.map do |k, v| + { name: k, enabled: v } end - # Each rule is se - def self.parse_global(input) - globals = {} - input.split("\n").reject { |line| - line =~ /---/ || line =~ /^\s*$/ - }.each { |line| + Puppet.debug("group rules #{transformed}") + transformed + end + + # Each rule is se + def self.parse_profile(input) + profile = {} + first_line = true + profile_name = '__error__' + input.split("\n").reject { |line| + line.include?('---') || line =~ %r{^\s*$} + }.each do |line| + if first_line + # take the first word in the line - eg "public profile settings" -> "public" + profile_name = line.split(' ')[0].downcase + first_line = false + else + # nasty hack - "firewall policy" setting contains space and will break our + # logic below. Also the setter in `netsh` to use is `firewallpolicy`. Just fix it... + line = line.sub('Firewall Policy', 'firewallpolicy') # split each line at most twice by first glob of whitespace - line_split = line.split(/\s+/, 2) + line_split = line.split(%r{\s+}, 2) if line_split.size == 2 key = key_name(line_split[0].strip) @@ -408,58 +377,82 @@ def self.parse_global(input) # downcase all values for comparison purposes value = line_split[1].strip.downcase - case key - when :secmethods - # secmethods are output with a hypen like this: - # DHGroup2-AES128-SHA1,DHGroup2-3DES-SHA1 - # but must be input with a colon like this: - # DHGroup2:AES128-SHA1,DHGroup2:3DES-SHA1 - safe_value = value.split(',').map { |e| - e.sub('-', ':') - }.join(',') - when :strongcrlcheck - safe_value = value.split(':')[0] - when :defaultexemptions - safe_value = value.split(',').sort - when :saidletimemin - safe_value = value.sub('min', '') - when :ipsecthroughnat - safe_value = value.gsub(' ', '') - else - safe_value = value - end - - globals[key] = safe_value + profile[key] = value end - } + end + end - globals[:name] = 'global' + # if we see the rule then it must exist... + profile[:name] = profile_name - Puppet.debug "Parsed windows firewall globals: #{globals}" - globals - end + Puppet.debug "Parsed windows firewall profile: #{profile}" + profile + end - # parse firewall profiles - def self.profiles(cmd) - profiles = [] - # the output of `show allprofiles` contains several blank lines that make parsing somewhat - # harder so just run it for each of the three profiles to make life easy... - ['publicprofile', 'domainprofile', 'privateprofile'].each { |profile| - profiles << parse_profile(Puppet::Util::Execution.execute([cmd, 'advfirewall', 'show', profile]).to_s) - } - profiles + # Each rule is se + def self.parse_global(input) + globals = {} + input.split("\n").reject { |line| + line.include?('---') || line =~ %r{^\s*$} + }.each do |line| + # split each line at most twice by first glob of whitespace + line_split = line.split(%r{\s+}, 2) + + next unless line_split.size == 2 + key = key_name(line_split[0].strip) + + # downcase all values for comparison purposes + value = line_split[1].strip.downcase + + safe_value = case key + when :secmethods + # secmethods are output with a hypen like this: + # DHGroup2-AES128-SHA1,DHGroup2-3DES-SHA1 + # but must be input with a colon like this: + # DHGroup2:AES128-SHA1,DHGroup2:3DES-SHA1 + value.split(',').map { |e| + e.sub('-', ':') + }.join(',') + when :strongcrlcheck + value.split(':')[0] + when :defaultexemptions + value.split(',').sort + when :saidletimemin + value.sub('min', '') + when :ipsecthroughnat + value.delete(' ') + else + value + end + + globals[key] = safe_value end + globals[:name] = 'global' + + Puppet.debug "Parsed windows firewall globals: #{globals}" + globals + end + + # parse firewall profiles + def self.profiles(cmd) + profiles = [] + # the output of `show allprofiles` contains several blank lines that make parsing somewhat + # harder so just run it for each of the three profiles to make life easy... + ['publicprofile', 'domainprofile', 'privateprofile'].each do |profile| + profiles << parse_profile(Puppet::Util::Execution.execute([cmd, 'advfirewall', 'show', profile]).to_s) + end + profiles + end - # parse firewall profiles - def self.globals(cmd) - profiles = [] - # the output of `show allprofiles` contains several blank lines that make parsing somewhat - # harder so just run it for each of the three profiles to make life easy... - ['publicprofile', 'domainprofile', 'privateprofile'].each { |profile| - profiles << parse_global(Puppet::Util::Execution.execute([cmd, 'advfirewall', 'show', 'global']).to_s) - } - profiles + # parse firewall profiles + def self.globals(cmd) + profiles = [] + # the output of `show allprofiles` contains several blank lines that make parsing somewhat + # harder so just run it for each of the three profiles to make life easy... + ['publicprofile', 'domainprofile', 'privateprofile'].each do |_profile| + profiles << parse_global(Puppet::Util::Execution.execute([cmd, 'advfirewall', 'show', 'global']).to_s) end + profiles end end diff --git a/lib/puppet_x/windows_firewall_ipsec.rb b/lib/puppet_x/windows_firewall_ipsec.rb index 29a484d..48d317b 100644 --- a/lib/puppet_x/windows_firewall_ipsec.rb +++ b/lib/puppet_x/windows_firewall_ipsec.rb @@ -1,253 +1,224 @@ require 'puppet_x' require 'pp' -module PuppetX - module WindowsFirewallIPSec - - MOD_DIR = 'windows_firewall/lib' - SCRIPT_FILE = 'ps-bridge-ipsec.ps1' - SCRIPT_PATH = File.join('ps/windows_firewall', SCRIPT_FILE) - - # We need to be able to invoke the PS bridge script in both agent and apply - # mode. In agent mode, the file will be found in LIBDIR, in apply mode it will - # be found somewhere under CODEDIR. We need to read from the appropriate dir - # for each mode to work in the most puppety way - def self.resolve_ps_bridge - - case Puppet.run_mode.name - when :user - # AKA `puppet resource` - first scan modules then cache - script = find_ps_bridge_in_modules || find_ps_bridge_in_cache - when :apply - # puppet apply demands local module install... - script = find_ps_bridge_in_modules - when :agent - # agent mode would only look in cache - script = find_ps_bridge_in_cache - else - raise("Don't know how to resolve #{SCRIPT_FILE} for windows_firewall in mode #{Puppet.run_mode.name}") - end - if ! script - raise("windows_firewall unable to find #{SCRIPT_FILE} in expected location") - end +# This module manage Windows Firewall IPSec rules +module PuppetX::WindowsFirewallIPSec + MOD_DIR = 'windows_firewall/lib'.freeze + SCRIPT_FILE = 'ps-bridge-ipsec.ps1'.freeze + SCRIPT_PATH = File.join('ps/windows_firewall', SCRIPT_FILE) + + # We need to be able to invoke the PS bridge script in both agent and apply + # mode. In agent mode, the file will be found in LIBDIR, in apply mode it will + # be found somewhere under CODEDIR. We need to read from the appropriate dir + # for each mode to work in the most puppety way + def self.resolve_ps_bridge + case Puppet.run_mode.name + when :user + # AKA `puppet resource` - first scan modules then cache + script = find_ps_bridge_in_modules || find_ps_bridge_in_cache + when :apply + # puppet apply demands local module install... + script = find_ps_bridge_in_modules + when :agent + # agent mode would only look in cache + script = find_ps_bridge_in_cache + else + raise("Don't know how to resolve #{SCRIPT_FILE} for windows_firewall in mode #{Puppet.run_mode.name}") + end - cmd = ['powershell.exe', '-ExecutionPolicy', 'Bypass', '-File', script] - cmd + unless script + raise("windows_firewall unable to find #{SCRIPT_FILE} in expected location") end - def self.find_ps_bridge_in_modules - # 1st priority - environment - check_for_script = File.join( - Puppet.settings[:environmentpath], - Puppet.settings[:environment], - 'modules', - MOD_DIR, - SCRIPT_PATH, - ) - Puppet.debug("Checking for #{SCRIPT_FILE} at #{check_for_script}") - if File.exists? check_for_script - script = check_for_script - else - # 2nd priority - custom module path, then basemodulepath - full_module_path = "#{Puppet.settings[:modulepath]}#{File::PATH_SEPARATOR}#{Puppet.settings[:basemodulepath]}" - full_module_path.split(File::PATH_SEPARATOR).reject do |path_element| - path_element.empty? - end.each do |path_element| - check_for_script = File.join(path_element, MOD_DIR, SCRIPT_PATH) - Puppet.debug("Checking for #{SCRIPT_FILE} at #{check_for_script}") - if File.exists? check_for_script - script = check_for_script - break; - end + cmd = ['powershell.exe', '-ExecutionPolicy', 'Bypass', '-File', script] + cmd + end + + def self.find_ps_bridge_in_modules + # 1st priority - environment + check_for_script = File.join( + Puppet.settings[:environmentpath], + Puppet.settings[:environment], + 'modules', + MOD_DIR, + SCRIPT_PATH, + ) + Puppet.debug("Checking for #{SCRIPT_FILE} at #{check_for_script}") + if File.exist? check_for_script + script = check_for_script + else + # 2nd priority - custom module path, then basemodulepath + full_module_path = "#{Puppet.settings[:modulepath]}#{File::PATH_SEPARATOR}#{Puppet.settings[:basemodulepath]}" + full_module_path.split(File::PATH_SEPARATOR).reject { |path_element| + path_element.empty? + }.each do |path_element| + check_for_script = File.join(path_element, MOD_DIR, SCRIPT_PATH) + Puppet.debug("Checking for #{SCRIPT_FILE} at #{check_for_script}") + if File.exist? check_for_script + script = check_for_script + break end end - - script end - def self.find_ps_bridge_in_cache - check_for_script = File.join(Puppet.settings[:libdir], SCRIPT_PATH) + script + end - Puppet.debug("Checking for #{SCRIPT_FILE} at #{check_for_script}") - script = File.exists?(check_for_script) ? check_for_script : nil - script - end + def self.find_ps_bridge_in_cache + check_for_script = File.join(Puppet.settings[:libdir], SCRIPT_PATH) - def self.to_ps(key) - { - :enabled => lambda { |x| camel_case(x) }, - :action => lambda { |x| camel_case(x) }, - :description => lambda { |x| x.empty? == true ? "\"#{x}\"" : x }, - :interface_type => lambda { |x| x.map { |e| camel_case(e)}.join(',') }, - :profile => lambda { |x| x.map { |e| camel_case(e)}.join(',') }, - :protocol => lambda { |x| x.to_s.upcase.sub('V', 'v') }, - :local_port => lambda { |x| x.kind_of?(Array) ? (x.map { |e| camel_case(e) }).join(',') : camel_case(x) }, - :remote_port => lambda { |x| x.kind_of?(Array) ? (x.map { |e| camel_case(e) }).join(',') : camel_case(x) }, - :local_address => lambda { |x| x.kind_of?(Array) ? (x.map { |e| camel_case(e) }).join(',') : camel_case(x) }, - :remote_address => lambda { |x| x.kind_of?(Array) ? (x.map { |e| camel_case(e) }).join(',') : camel_case(x) }, - :mode => lambda { |x| camel_case(x) }, - :inbound_security => lambda { |x| camel_case(x) }, - :outbound_security => lambda { |x| camel_case(x) }, - :phase1auth_set => lambda { |x| camel_case(x) }, - :phase2auth_set => lambda { |x| camel_case(x) }, - }.fetch(key, lambda { |x| x }) - end + Puppet.debug("Checking for #{SCRIPT_FILE} at #{check_for_script}") + script = File.exist?(check_for_script) ? check_for_script : nil + script + end - def self.to_ruby(key) - { - :enabled => lambda { |x| snake_case_sym(x) }, - :action => lambda { |x| snake_case_sym(x) }, - :interface_type => lambda { |x| x.split(',').map{ |e| snake_case_sym(e.strip) } }, - :profile => lambda { |x| x.split(',').map{ |e| snake_case_sym(e.strip) } }, - :protocol => lambda { |x| snake_case_sym(x) }, - :remote_port => lambda { |x| x.kind_of?(Array) ? x.map { |e| e.downcase } : x.downcase.split }, - :local_port => lambda { |x| x.kind_of?(Array) ? x.map { |e| e.downcase } : x.downcase.split }, - :remote_address => lambda { |x| x.kind_of?(Array) ? x.map { |e| e.downcase } : x.downcase.split }, - :local_address => lambda { |x| x.kind_of?(Array) ? x.map { |e| e.downcase } : x.downcase.split }, - :mode => lambda { |x| x.downcase }, - :inbound_security => lambda { |x| x.downcase }, - :outbound_security => lambda { |x| x.downcase }, - :phase1auth_set => lambda { |x| x.downcase }, - :phase2auth_set => lambda { |x| x.downcase }, - }.fetch(key, lambda { |x| x }) - end + def self.to_ps(key) + { + enabled: ->(x) { camel_case(x) }, + action: ->(x) { camel_case(x) }, + description: ->(x) { (x.empty? == true) ? "\"#{x}\"" : x }, + interface_type: ->(x) { x.map { |e| camel_case(e) }.join(',') }, + profile: ->(x) { x.map { |e| camel_case(e) }.join(',') }, + protocol: ->(x) { x.to_s.upcase.sub('V', 'v') }, + local_port: ->(x) { x.is_a?(Array) ? (x.map { |e| camel_case(e) }).join(',') : camel_case(x) }, + remote_port: ->(x) { x.is_a?(Array) ? (x.map { |e| camel_case(e) }).join(',') : camel_case(x) }, + local_address: ->(x) { x.is_a?(Array) ? (x.map { |e| camel_case(e) }).join(',') : camel_case(x) }, + remote_address: ->(x) { x.is_a?(Array) ? (x.map { |e| camel_case(e) }).join(',') : camel_case(x) }, + mode: ->(x) { camel_case(x) }, + inbound_security: ->(x) { camel_case(x) }, + outbound_security: ->(x) { camel_case(x) }, + phase1auth_set: ->(x) { camel_case(x) }, + phase2auth_set: ->(x) { camel_case(x) }, + }.fetch(key, ->(x) { x }) + end - # create a normalised key name by: - # 1. lowercasing input - # 2. converting spaces to underscores - # 3. convert to symbol - def self.key_name(input) - input.downcase.gsub(/\s/, '_').to_sym - end + def self.to_ruby(key) + { + enabled: ->(x) { snake_case_sym(x) }, + action: ->(x) { snake_case_sym(x) }, + interface_type: ->(x) { x.split(',').map { |e| snake_case_sym(e.strip) } }, + profile: ->(x) { x.split(',').map { |e| snake_case_sym(e.strip) } }, + protocol: ->(x) { snake_case_sym(x) }, + remote_port: ->(x) { x.is_a?(Array) ? x.map { |e| e.downcase } : x.downcase.split }, + local_port: ->(x) { x.is_a?(Array) ? x.map { |e| e.downcase } : x.downcase.split }, + remote_address: ->(x) { x.is_a?(Array) ? x.map { |e| e.downcase } : x.downcase.split }, + local_address: ->(x) { x.is_a?(Array) ? x.map { |e| e.downcase } : x.downcase.split }, + mode: ->(x) { x.downcase }, + inbound_security: ->(x) { x.downcase }, + outbound_security: ->(x) { x.downcase }, + phase1auth_set: ->(x) { x.downcase }, + phase2auth_set: ->(x) { x.downcase }, + }.fetch(key, ->(x) { x }) + end - # Convert input CamelCase to snake_case symbols - def self.snake_case_sym(input) - input.gsub(/([a-z])([A-Z])/, '\1_\2').downcase.to_sym - end + # create a normalised key name by: + # 1. lowercasing input + # 2. converting spaces to underscores + # 3. convert to symbol + def self.key_name(input) + input.downcase.gsub(%r{\s}, '_').to_sym + end - # Convert snake_case input symbol to CamelCase string - def self.camel_case(input) - # https://stackoverflow.com/a/24917606/3441106 - input.to_s.split('_').collect(&:capitalize).join - end + # Convert input CamelCase to snake_case symbols + def self.snake_case_sym(input) + input.gsub(%r{([a-z])([A-Z])}, '\1_\2').downcase.to_sym + end - def self.delete_rule(resource) - Puppet.notice("(windows_firewall) deleting ipsec rule '#{resource[:display_name]}'") - out = Puppet::Util::Execution.execute(resolve_ps_bridge + ['delete', resource[:name]]).to_s - Puppet.debug out - end + # Convert snake_case input symbol to CamelCase string + def self.camel_case(input) + # https://stackoverflow.com/a/24917606/3441106 + input.to_s.split('_').map(&:capitalize).join + end - def self.update_rule(resource) - Puppet.notice("(windows_firewall) updating ipsec rule '#{resource[:display_name]}'") + def self.delete_rule(resource) + Puppet.notice("(windows_firewall) deleting ipsec rule '#{resource[:display_name]}'") + out = Puppet::Util::Execution.execute(resolve_ps_bridge + ['delete', resource[:name]]).to_s + Puppet.debug out + end - # `Name` is mandatory and also a `parameter` not a `property` - args = [ '-Name', resource[:name] ] + def self.update_rule(resource) + Puppet.notice("(windows_firewall) updating ipsec rule '#{resource[:display_name]}'") - resource.properties.reject { |property| - [:ensure, :protocol_type, :protocol_code].include?(property.name) - }.each { |property| - # All properties start `-` - property_name = "-#{camel_case(property.name)}" - property_value = to_ps(property.name).call(property.value) + # `Name` is mandatory and also a `parameter` not a `property` + args = [ '-Name', resource[:name] ] - # protocol can optionally specify type and code, other properties are set very simply - args << property_name - args << property_value - } - Puppet.debug "Updating firewall ipsec rule with args: #{args}" + resource.properties.reject { |property| + [:ensure, :protocol_type, :protocol_code].include?(property.name) + }.each do |property| + # All properties start `-` + property_name = "-#{camel_case(property.name)}" + property_value = to_ps(property.name).call(property.value) - out = Puppet::Util::Execution.execute(resolve_ps_bridge + ['update'] + args) - Puppet.debug out + # protocol can optionally specify type and code, other properties are set very simply + args << property_name + args << property_value end + Puppet.debug "Updating firewall ipsec rule with args: #{args}" - # Create a new firewall rule using powershell - # @see https://docs.microsoft.com/en-us/powershell/module/netsecurity/new-netfirewallrule?view=win10-ps - def self.create_rule(resource) - Puppet.notice("(windows_firewall) adding ipsec rule '#{resource[:display_name]}'") - - # `Name` is mandatory and also a `parameter` not a `property` - args = [ '-Name', resource[:name] ] - - resource.properties.reject { |property| - [:ensure, :protocol_type, :protocol_code].include?(property.name) - }.each { |property| - # All properties start `-` - property_name = "-#{camel_case(property.name)}" - property_value = to_ps(property.name).call(property.value) - - # protocol can optionally specify type and code, other properties are set very simply - args << property_name - args << property_value - } - Puppet.debug "Creating firewall ipsec rule with args: #{args}" - - out = Puppet::Util::Execution.execute(resolve_ps_bridge + ['create'] + args) - Puppet.debug out - end + out = Puppet::Util::Execution.execute(resolve_ps_bridge + ['update'] + args) + Puppet.debug out + end - def self.rules - Puppet.debug('query all ipsec rules') - rules = JSON.parse Puppet::Util::Execution.execute(resolve_ps_bridge + ['show']).to_s - - # Rules is an array of hash as-parsed and hash keys need converted to - # lowercase ruby labels - puppet_rules = rules.map { |e| - Hash[e.map { |k, v| - key = snake_case_sym(k) - [key, to_ruby(key).call(v)] - }].merge({ensure: :present}) - } - Puppet.debug("Parsed ipsec rules: #{puppet_rules.size}") - puppet_rules - end + # Create a new firewall rule using powershell + # @see https://docs.microsoft.com/en-us/powershell/module/netsecurity/new-netfirewallrule?view=win10-ps + def self.create_rule(resource) + Puppet.notice("(windows_firewall) adding ipsec rule '#{resource[:display_name]}'") - # Each rule is se - def self.parse_profile(input) - profile = {} - first_line = true - profile_name = '__error__' - input.split("\n").reject { |line| - line =~ /---/ || line =~ /^\s*$/ - }.each { |line| - if first_line - # take the first word in the line - eg "public profile settings" -> "public" - profile_name = line.split(" ")[0].downcase - first_line = false - else - # nasty hack - "firewall policy" setting contains space and will break our - # logic below. Also the setter in `netsh` to use is `firewallpolicy`. Just fix it... - line = line.sub('Firewall Policy', 'firewallpolicy') - - # split each line at most twice by first glob of whitespace - line_split = line.split(/\s+/, 2) - - if line_split.size == 2 - key = key_name(line_split[0].strip) - - # downcase all values for comparison purposes - value = line_split[1].strip.downcase - - profile[key] = value - end - end - } + # `Name` is mandatory and also a `parameter` not a `property` + args = [ '-Name', resource[:name] ] - # if we see the rule then it must exist... - profile[:name] = profile_name + resource.properties.reject { |property| + [:ensure, :protocol_type, :protocol_code].include?(property.name) + }.each do |property| + # All properties start `-` + property_name = "-#{camel_case(property.name)}" + property_value = to_ps(property.name).call(property.value) - Puppet.debug "Parsed windows firewall profile: #{profile}" - profile + # protocol can optionally specify type and code, other properties are set very simply + args << property_name + args << property_value end + Puppet.debug "Creating firewall ipsec rule with args: #{args}" - # Each rule is se - def self.parse_global(input) - globals = {} - input.split("\n").reject { |line| - line =~ /---/ || line =~ /^\s*$/ - }.each { |line| + out = Puppet::Util::Execution.execute(resolve_ps_bridge + ['create'] + args) + Puppet.debug out + end + + def self.rules + Puppet.debug('query all ipsec rules') + rules = JSON.parse Puppet::Util::Execution.execute(resolve_ps_bridge + ['show']).to_s + + # Rules is an array of hash as-parsed and hash keys need converted to + # lowercase ruby labels + puppet_rules = rules.map do |e| + Hash[e.map do |k, v| + key = snake_case_sym(k) + [key, to_ruby(key).call(v)] + end].merge({ ensure: :present }) + end + Puppet.debug("Parsed ipsec rules: #{puppet_rules.size}") + puppet_rules + end + + # Each rule is se + def self.parse_profile(input) + profile = {} + first_line = true + profile_name = '__error__' + input.split("\n").reject { |line| + line.include?('---') || line =~ %r{^\s*$} + }.each do |line| + if first_line + # take the first word in the line - eg "public profile settings" -> "public" + profile_name = line.split(' ')[0].downcase + first_line = false + else + # nasty hack - "firewall policy" setting contains space and will break our + # logic below. Also the setter in `netsh` to use is `firewallpolicy`. Just fix it... + line = line.sub('Firewall Policy', 'firewallpolicy') # split each line at most twice by first glob of whitespace - line_split = line.split(/\s+/, 2) + line_split = line.split(%r{\s+}, 2) if line_split.size == 2 key = key_name(line_split[0].strip) @@ -255,35 +226,60 @@ def self.parse_global(input) # downcase all values for comparison purposes value = line_split[1].strip.downcase - case key - when :secmethods - # secmethods are output with a hypen like this: - # DHGroup2-AES128-SHA1,DHGroup2-3DES-SHA1 - # but must be input with a colon like this: - # DHGroup2:AES128-SHA1,DHGroup2:3DES-SHA1 - safe_value = value.split(',').map { |e| - e.sub('-', ':') - }.join(',') - when :strongcrlcheck - safe_value = value.split(':')[0] - when :defaultexemptions - safe_value = value.split(',').sort - when :saidletimemin - safe_value = value.sub('min', '') - when :ipsecthroughnat - safe_value = value.gsub(' ', '') - else - safe_value = value - end - - globals[key] = safe_value + profile[key] = value end - } + end + end - globals[:name] = 'global' + # if we see the rule then it must exist... + profile[:name] = profile_name - Puppet.debug "Parsed windows firewall globals: #{globals}" - globals + Puppet.debug "Parsed windows firewall profile: #{profile}" + profile + end + + # Each rule is se + def self.parse_global(input) + globals = {} + input.split("\n").reject { |line| + line.include?('---') || line =~ %r{^\s*$} + }.each do |line| + # split each line at most twice by first glob of whitespace + line_split = line.split(%r{\s+}, 2) + + next unless line_split.size == 2 + key = key_name(line_split[0].strip) + + # downcase all values for comparison purposes + value = line_split[1].strip.downcase + + safe_value = case key + when :secmethods + # secmethods are output with a hypen like this: + # DHGroup2-AES128-SHA1,DHGroup2-3DES-SHA1 + # but must be input with a colon like this: + # DHGroup2:AES128-SHA1,DHGroup2:3DES-SHA1 + value.split(',').map { |e| + e.sub('-', ':') + }.join(',') + when :strongcrlcheck + value.split(':')[0] + when :defaultexemptions + value.split(',').sort + when :saidletimemin + value.sub('min', '') + when :ipsecthroughnat + value.delete(' ') + else + value + end + + globals[key] = safe_value end + + globals[:name] = 'global' + + Puppet.debug "Parsed windows firewall globals: #{globals}" + globals end end diff --git a/metadata.json b/metadata.json index d2cfc61..cb0d49f 100644 --- a/metadata.json +++ b/metadata.json @@ -1,6 +1,6 @@ { "name": "webalex-windows_firewall", - "version": "1.4.1", + "version": "1.4.2", "author": "webalex", "summary": "Manage the windows firewall with Puppet", "license": "Apache-2.0",