Class: Inspec::Resources::NfTables

Inherits:
Object
  • Object
show all
Defined in:
lib/inspec/resources/nftables.rb

Constant Summary collapse

@@bin =
nil
@@nft_params =
{}

Instance Method Summary collapse

Constructor Details

#initialize(params = {}) ⇒ NfTables

Returns a new instance of NfTables.



35
36
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
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/inspec/resources/nftables.rb', line 35

def initialize(params = {})
  @family = params[:family] || nil
  @table = params[:table] || nil
  @chain = params[:chain] || nil
  @set = params[:set] || nil
  @ignore_comments = params[:ignore_comments] || false
  unless @@bin
    @@bin = find_nftables_or_error
  end

  # Some old versions of `nft` do not support JSON output or stateless modifier
  res = inspec.command("#{@@bin} --version").stdout
  version = Gem::Version.new(/^nftables v(\S+) .*/.match(res)[1])
  case
  when version < Gem::Version.new("0.8.0")
    @@nft_params["num"] = "-nn"
  when version < Gem::Version.new("0.9.0")
    @@nft_params["stateless"] = "-s"
    @@nft_params["num"] = "-nn"
  when version < Gem::Version.new("0.9.3")
    @@nft_params["json"] = "-j"
    @@nft_params["stateless"] = "-s"
    @@nft_params["num"] = "-nn"
  when version >= Gem::Version.new("0.9.3")
    @@nft_params["json"] = "-j"
    @@nft_params["stateless"] = "-s"
    @@nft_params["num"] = "-y"
    ## --terse
  end

  # family and table attributes are mandatory
  fail_resource "nftables family and table are mandatory." if @family.nil? || @family.empty? || @table.nil? || @table.empty?
  # chain name or set name has to be specified and are mutually exclusive
  fail_resource "You must specify either a chain or a set name." if (@chain.nil? || @chain.empty?) && (@set.nil? || @set.empty?)
  fail_resource "You must specify either a chain or a set name, not both." if !(@chain.nil? || @chain.empty?) && !(@set.nil? || @set.empty?)

  # we're done if we are on linux
  return if inspec.os.linux?

  # ensures, all calls are aborted for non-supported os
  @nftables_cache = {}
  skip_resource "The `nftables` resource is not supported on your OS yet."
end

Instance Method Details

#_get_attr(name) ⇒ Object

Let’s have a generic method to retrieve attributes for chains and sets



80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
# File 'lib/inspec/resources/nftables.rb', line 80

def _get_attr(name)
  # Some attributes are valid for chains only, for sets only or for both
  valid = {
    "chains" => %w{hook policy prio type},
    "sets" => %w{flags size type},
  }

  target_obj = @set.nil? ? "chains" : "sets"

  if valid[target_obj].include?(name)
    attrs = @set.nil? ? retrieve_chain_attrs : retrieve_set_attrs
  else
    raise Inspec::Exceptions::ResourceSkipped, "`#{name}` attribute is not valid for #{target_obj}"
  end
  # flags attribute is an array, if not retrieved ensure we return an empty array
  # otherwise return an empty string
  default = name == "flags" ? [] : ""
  val = attrs.key?(name) ? attrs[name] : default
  # When set type is has multiple data types it's retrieved as an array, make humans life easier
  # by returning a string representation
  if name == "type" && target_obj == "sets" && val.is_a?(Array)
    return val.join(" . ")
  end

  val
end

#has_element?(element = nil, _family = nil, _table = nil, _chain = nil) ⇒ Boolean

Returns:

  • (Boolean)


120
121
122
123
124
# File 'lib/inspec/resources/nftables.rb', line 120

def has_element?(element = nil, _family = nil, _table = nil, _chain = nil)
  # checks if the element is part of the set
  # for now, we expect an exact match
  retrieve_set_elements.any? { |line| line.casecmp(element) == 0 }
end

#has_rule?(rule = nil, _family = nil, _table = nil, _chain = nil) ⇒ Boolean

Returns:

  • (Boolean)


114
115
116
117
118
# File 'lib/inspec/resources/nftables.rb', line 114

def has_rule?(rule = nil, _family = nil, _table = nil, _chain = nil)
  # checks if the rule is part of the chain
  # for now, we expect an exact match
  retrieve_chain_rules.any? { |line| line.casecmp(rule) == 0 }
end

#resource_idObject



236
237
238
# File 'lib/inspec/resources/nftables.rb', line 236

def resource_id
  to_s || "nftables"
end

#retrieve_chain_attrsObject



178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
# File 'lib/inspec/resources/nftables.rb', line 178

def retrieve_chain_attrs
  idx = "chain_attrs_#{@family}_#{@table}_#{@chain}"
  return @nftables_cache[idx] if defined?(@nftables_cache) && @nftables_cache.key?(idx)

  @nftables_cache = {} unless defined?(@nftables_cache)

  chain_cmd = "list chain #{@family} #{@table} #{@chain}"
  nftables_cmd = format("%s %s %s %s", @@bin, @@nft_params["stateless"], @@nft_params["json"], chain_cmd).strip

  cmd = inspec.command(nftables_cmd)
  return {} if cmd.exit_status.to_i != 0

  if @@nft_params["json"].empty?
    res = cmd.stdout.gsub("\t", "").split("\n").select { |line| line =~ /^type/ }[0]
    parsed = /type (\S+) hook (\S+) priority (\S+); policy (\S+);/.match(res)
    @nftables_cache[idx] = { "type" => parsed[1], "hook" => parsed[2], "prio" => parsed[3].to_i, "policy" => parsed[4] }
  else
    @nftables_cache[idx] = JSON.parse(cmd.stdout)["nftables"].select { |line| line.key?("chain") }[0]["chain"]
  end
end

#retrieve_chain_rulesObject



154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/inspec/resources/nftables.rb', line 154

def retrieve_chain_rules
  idx = "rule_#{@family}_#{@table}_#{@chain}"
  return @nftables_cache[idx] if defined?(@nftables_cache) && @nftables_cache.key?(idx)

  @nftables_cache = {} unless defined?(@nftables_cache)

  # construct nftables command to read all rules of the given chain
  chain_cmd = "list chain #{@family} #{@table} #{@chain}"
  nftables_cmd = format("%s %s %s %s", @@bin, @@nft_params["stateless"], @@nft_params["num"], chain_cmd).strip

  cmd = inspec.command(nftables_cmd)
  return [] if cmd.exit_status.to_i != 0

  rules = cmd.stdout.gsub("\t", "").split("\n").reject { |line| line =~ /^(table|chain)/ || line =~ /^}$/ }

  if @ignore_comments
    # split rules, returns array or rules without any comment
    @nftables_cache[idx] = remove_comments_from_rules(rules)
  else
    # split rules, returns array or rules
    @nftables_cache[idx] = rules.map(&:strip)
  end
end

#retrieve_set_attrsObject



199
200
201
202
203
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
# File 'lib/inspec/resources/nftables.rb', line 199

def retrieve_set_attrs
  idx = "set_attrs_#{@family}_#{@table}_#{@chain}"
  return @nftables_cache[idx] if defined?(@nftables_cache) && @nftables_cache.key?(idx)

  @nftables_cache = {} unless defined?(@nftables_cache)

  chain_cmd = "list set #{@family} #{@table} #{@set}"
  nftables_cmd = format("%s %s %s %s", @@bin, @@nft_params["stateless"], @@nft_params["json"], chain_cmd).strip

  cmd = inspec.command(nftables_cmd)
  return {} if cmd.exit_status.to_i != 0

  if @@nft_params["json"].empty?
    type = ""
    size = 0
    flags = []
    res = cmd.stdout.gsub("\t", "").split("\n").select { |line| line =~ /^(type|size|flags)/ }
    res.each do |line|
      parsed = /^type (.*)/.match(line)
      if parsed
        type = parsed[1]
      end
      parsed = /^flags (.*)/.match(line)
      if parsed
        flags = parsed[1].split(",")
      end
      parsed = /^size (.*)/.match(line)
      if parsed
        size = parsed[1].to_i
      end
    end
    @nftables_cache[idx] = { "type" => type, "size" => size, "flags" => flags }
  else
    @nftables_cache[idx] = JSON.parse(cmd.stdout)["nftables"].select { |line| line.key?("set") }[0]["set"]
  end
end

#retrieve_set_elementsObject



126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'lib/inspec/resources/nftables.rb', line 126

def retrieve_set_elements
  idx = "set_#{@family}_#{@table}_#{@set}"
  return @nftables_cache[idx] if defined?(@nftables_cache) && @nftables_cache.key?(idx)

  @nftables_cache = {} unless defined?(@nftables_cache)

  elem_cmd = "list set #{@family} #{@table} #{@set}"
  nftables_cmd = format("%s %s %s", @@bin, @@nft_params["stateless"], elem_cmd).strip

  cmd = inspec.command(nftables_cmd)
  return [] if cmd.exit_status.to_i != 0

  # https://github.com/inspec/inspec/security/code-scanning/10
  # Update @nftables_cache with sanitized command output
  @nftables_cache[idx] = cmd.stdout.gsub("\t", "").split("\n")
    .reject { |line| line =~ /^(table|set|type|size|flags|typeof|auto-merge)/ || line =~ /^}$/ } # Reject lines that match certain patterns
    .map { |line| line.gsub("elements = {", "").gsub("}", "").split(",") } # Use gsub to replace all occurrences of specified strings
    .flatten # Flatten the array of arrays into a single array
    .map(&:strip) # Remove leading and trailing whitespace from each element
    .map { |element| sanitize_input(element) } # Sanitize each element to prevent injection attacks
end

#sanitize_input(input) ⇒ Object

Method to sanitize input



149
150
151
152
# File 'lib/inspec/resources/nftables.rb', line 149

def sanitize_input(input)
  # Replace potentially dangerous characters with their escaped counterparts
  input.gsub(/([\\'";])/, '\\\\\1')
end

#to_sObject



240
241
242
# File 'lib/inspec/resources/nftables.rb', line 240

def to_s
  format("nftables (%s %s %s %s)", @family && "family: #{@family}", @table && "table: #{@table}", @chain && "chain: #{@chain}", @set && "set: #{@set}").strip
end