Module: PuppetX::WindowsFirewallIPSec

Defined in:
lib/puppet_x/windows_firewall_ipsec.rb

Overview

This module manage Windows Firewall IPSec rules

Constant Summary collapse

MOD_DIR =
'windows_firewall/lib'.freeze
SCRIPT_FILE =
'ps-bridge-ipsec.ps1'.freeze
SCRIPT_PATH =
File.join('ps/windows_firewall', SCRIPT_FILE)

Class Method Summary collapse

Class Method Details

.camel_case(input) ⇒ Object

Convert snake_case input symbol to CamelCase string



128
129
130
131
# File 'lib/puppet_x/windows_firewall_ipsec.rb', line 128

def self.camel_case(input)
  # https://stackoverflow.com/a/24917606/3441106
  input.to_s.split('_').map(&:capitalize).join
end

.create_rule(resource) ⇒ Object

Create a new firewall rule using powershell



164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
# File 'lib/puppet_x/windows_firewall_ipsec.rb', line 164

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 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 ipsec rule with args: #{args}"

  out = Puppet::Util::Execution.execute(resolve_ps_bridge + ['create'] + args)
  Puppet.debug out
end

.delete_rule(resource) ⇒ Object



133
134
135
136
137
# File 'lib/puppet_x/windows_firewall_ipsec.rb', line 133

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

.find_ps_bridge_in_cacheObject



67
68
69
70
71
72
73
# File 'lib/puppet_x/windows_firewall_ipsec.rb', line 67

def self.find_ps_bridge_in_cache
  check_for_script = File.join(Puppet.settings[:libdir], SCRIPT_PATH)

  Puppet.debug("Checking for #{SCRIPT_FILE} at #{check_for_script}")
  script = File.exist?(check_for_script) ? check_for_script : nil
  script
end

.find_ps_bridge_in_modulesObject



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/puppet_x/windows_firewall_ipsec.rb', line 37

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
  end

  script
end

.key_name(input) ⇒ Object

create a normalised key name by:

  1. lowercasing input

  2. converting spaces to underscores

  3. convert to symbol



118
119
120
# File 'lib/puppet_x/windows_firewall_ipsec.rb', line 118

def self.key_name(input)
  input.downcase.gsub(%r{\s}, '_').to_sym
end

.parse_global(input) ⇒ Object

Each rule is se



242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
# File 'lib/puppet_x/windows_firewall_ipsec.rb', line 242

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_profile(input) ⇒ Object

Each rule is se



204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
# File 'lib/puppet_x/windows_firewall_ipsec.rb', line 204

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(%r{\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
  end

  # if we see the rule then it must exist...
  profile[:name] = profile_name

  Puppet.debug "Parsed windows firewall profile: #{profile}"
  profile
end

.resolve_ps_bridgeObject

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



14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# File 'lib/puppet_x/windows_firewall_ipsec.rb', line 14

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

  unless script
    raise("windows_firewall unable to find #{SCRIPT_FILE} in expected location")
  end

  cmd = ['powershell.exe', '-ExecutionPolicy', 'Bypass', '-File', script]
  cmd
end

.rulesObject



187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
# File 'lib/puppet_x/windows_firewall_ipsec.rb', line 187

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

.snake_case_sym(input) ⇒ Object

Convert input CamelCase to snake_case symbols



123
124
125
# File 'lib/puppet_x/windows_firewall_ipsec.rb', line 123

def self.snake_case_sym(input)
  input.gsub(%r{([a-z])([A-Z])}, '\1_\2').downcase.to_sym
end

.to_ps(key) ⇒ Object



75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/puppet_x/windows_firewall_ipsec.rb', line 75

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

.to_ruby(key) ⇒ Object



95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
# File 'lib/puppet_x/windows_firewall_ipsec.rb', line 95

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

.update_rule(resource) ⇒ Object



139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/puppet_x/windows_firewall_ipsec.rb', line 139

def self.update_rule(resource)
  Puppet.notice("(windows_firewall) updating 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 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 "Updating firewall ipsec rule with args: #{args}"

  out = Puppet::Util::Execution.execute(resolve_ps_bridge + ['update'] + args)
  Puppet.debug out
end