mirror of
https://github.com/inspec/inspec
synced 2024-11-21 20:23:06 +00:00
* Fix systemd path for Leap image * Use vhef client version 17 as doocker cookbook do not support >= 18 * Add nftables resource * Add nftables tests * Add fixtures for nftables tests * enable nftables only when attr is true - then disable iptables * By default test iptables, not nftables * Fix tests and lint errors * Increase unit test coverage for nftables * Do not use -nn nft option as behaviour changes based on nft version * Base nft params identification on its version, not os version Signed-off-by: Jeremy JACQUE <jeremy.jacque@algolia.com> * Make test more human friendly by reversing unless/if logic Signed-off-by: Jeremy JACQUE <jeremy.jacque@algolia.com> * Update mocked cmds with nft params Signed-off-by: Jeremy JACQUE <jeremy.jacque@algolia.com> * Fix quoting issue with rubocop * Fix uninitiallized class vars * Fix unit test by adding nft version mocking * Clean nftables doc --------- Signed-off-by: Jeremy JACQUE <jeremy.jacque@algolia.com> Co-authored-by: jjacque <jeremy.jacque@algolia.com>
This commit is contained in:
parent
f51da83bf9
commit
4fce6845e5
18 changed files with 515 additions and 4 deletions
140
docs-chef-io/content/inspec/resources/nftables.md
Normal file
140
docs-chef-io/content/inspec/resources/nftables.md
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
+++
|
||||||
|
title = "nftables resource"
|
||||||
|
draft = false
|
||||||
|
gh_repo = "inspec"
|
||||||
|
platform = "linux"
|
||||||
|
|
||||||
|
[menu]
|
||||||
|
[menu.inspec]
|
||||||
|
title = "nftables"
|
||||||
|
identifier = "inspec/resources/os/nftables.md nftables resource"
|
||||||
|
parent = "inspec/resources/os"
|
||||||
|
+++
|
||||||
|
|
||||||
|
Use the `nftables` Chef InSpec audit resource to test rules and sets that are defined using `nftables`, which maintains tables of IP packet filtering rules. There may be more than one table. Each table contains one (or more) chains. A chain is a list of rules that match packets. When a rule matches a packet, the rule defines what target to assign to the packet.
|
||||||
|
|
||||||
|
## Availability
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
{{% inspec/inspec_installation %}}
|
||||||
|
|
||||||
|
### Version
|
||||||
|
|
||||||
|
This resource first became available in v5.21.30 of InSpec.
|
||||||
|
|
||||||
|
## Syntax
|
||||||
|
|
||||||
|
A `nftables` resource block declares tests for rules in IP tables:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
describe nftables(family:'name', table:'name', chain: 'name') do
|
||||||
|
its('PROPERTY') { should eq 'value' }
|
||||||
|
it { should have_rule('RULE') }
|
||||||
|
end
|
||||||
|
|
||||||
|
describe nftables(family:'name', table:'name', set: 'name') do
|
||||||
|
its('PROPERTY') { should eq 'value' }
|
||||||
|
it { should have_element('ELEMENT') }
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
where
|
||||||
|
|
||||||
|
- `nftables()` has to specify `family` and `table`. It also has to specify one of `chain` or `set` (exclusively).
|
||||||
|
- `family:'name'` is the name of the `family` the table belongs to, one of `ip`, `ip6`, `inet`, `arp`, `bridge`, `netdev`.
|
||||||
|
- `table:'name'` is the packet matching table against which the test is run.
|
||||||
|
- `chain: 'name'` is the name of a user-defined chain.
|
||||||
|
- `set: 'name'` is the name of a user-defined named set.
|
||||||
|
- `have_rule('RULE')` tests that the chain has a given rule in the nftables ruleset. This must match the entire line taken from `nftables -nn list chain FAMILY TABLE CHAIN`.
|
||||||
|
- `have_element('ELEMENT')` tests that element is a member of the nftables named set.
|
||||||
|
|
||||||
|
See the [NFT man page](https://www.netfilter.org/projects/nftables/manpage.html) and [nftables wiki](https://wiki.nftables.org/wiki-nftables/index.php/Main_Page) for more information about nftables.
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
|
||||||
|
### Chain Properties
|
||||||
|
|
||||||
|
`hook`
|
||||||
|
: The hook type. Possible values: `ingress`, `prerouting`, `forward`, `input`, `output`, `postrouting`, and `egress`.
|
||||||
|
|
||||||
|
`prio`
|
||||||
|
: The numerical chain priority.
|
||||||
|
|
||||||
|
`policy`
|
||||||
|
: The policy type. Possible values: `accept`, `drop`.
|
||||||
|
|
||||||
|
`type`
|
||||||
|
: The chain type. Possible values: `filter`, `nat`, and `route`.
|
||||||
|
|
||||||
|
### Set Properties
|
||||||
|
|
||||||
|
`flags`
|
||||||
|
: The set flags. Possible values: `constant`, `dynamic`, `interval`, and `timeout`.
|
||||||
|
|
||||||
|
`size`
|
||||||
|
: The maximum number of elements in the set.
|
||||||
|
|
||||||
|
`type`
|
||||||
|
: The data type of set elements. Possible values: `ipv4_addr`, `ipv6_addr`, `ether_addr`, `inet_proto`, `inet_service`, and `mark`.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
The following examples show how to use this Chef InSpec audit resource.
|
||||||
|
|
||||||
|
### Test if the `CHAIN_NAME` chain from the `TABLE_NAME` table has the default `accept` policy
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
describe nftables(family: 'inet', table: 'TABLE_NAME', chain: 'CHAIN_NAME') do
|
||||||
|
its('policy') { should eq 'accept' }
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test the attributes of the `CHAIN_NAME` chain from the `TABLE_NAME` table
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
describe nftables(family: 'inet', table: 'mangle', chain: 'INPUT') do
|
||||||
|
its('type') { should eq 'filter' }
|
||||||
|
its('hook') { should eq 'input' }
|
||||||
|
its('prio') { should eq (-150) } # mangle
|
||||||
|
its('policy') { should eq 'accept' }
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test if there is a rule allowing Postgres (5432/TCP) traffic
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
describe nftables(family: 'inet', table: 'TABLE_NAME', chain: 'CHAIN_NAME') do
|
||||||
|
it { should have_rule('tcp dport 5432 comment "postgres" accept') }
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that the rule specification must exactly match what's in the output of `nftables -nn list chain inet TABLE_NAME CHAIN_NAME`, which will depend on how you've built your rules.
|
||||||
|
|
||||||
|
### Test if there is an element `1.1.1.1` in the `SET_NAME` named set
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
describe nftables(family: 'inet', table: 'TABLE_NAME', set: 'SET_NAME') do
|
||||||
|
it { should have_element('1.1.1.1') }
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
## Matchers
|
||||||
|
|
||||||
|
For a full list of available matchers, please visit our [matchers page](/inspec/matchers/).
|
||||||
|
|
||||||
|
### have_rule
|
||||||
|
|
||||||
|
The `have_rule` matcher tests the named rule against the information in the `nftables` ruleset:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
it { should have_rule('RULE') }
|
||||||
|
```
|
||||||
|
|
||||||
|
### have_element
|
||||||
|
|
||||||
|
The `have_element` matcher tests the named set against the information in the `nftables` ruleset:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
it { should have_element('SET_ELEMENT') }
|
||||||
|
```
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
driver:
|
driver:
|
||||||
name: dokken
|
name: dokken
|
||||||
chef_version: :latest
|
chef_version: 17
|
||||||
privileged: true # because Docker and SystemD/Upstart
|
privileged: true # because Docker and SystemD/Upstart
|
||||||
|
|
||||||
transport:
|
transport:
|
||||||
|
@ -64,7 +64,7 @@ platforms:
|
||||||
- name: opensuse-leap
|
- name: opensuse-leap
|
||||||
driver:
|
driver:
|
||||||
image: dokken/opensuse-leap-15
|
image: dokken/opensuse-leap-15
|
||||||
pid_one_command: /bin/systemd
|
pid_one_command: /usr/lib/systemd/systemd
|
||||||
|
|
||||||
- name: ubuntu-16.04
|
- name: ubuntu-16.04
|
||||||
driver:
|
driver:
|
||||||
|
|
|
@ -73,6 +73,7 @@ require "inspec/resources/mssql_sys_conf"
|
||||||
require "inspec/resources/mysql"
|
require "inspec/resources/mysql"
|
||||||
require "inspec/resources/mysql_conf"
|
require "inspec/resources/mysql_conf"
|
||||||
require "inspec/resources/mysql_session"
|
require "inspec/resources/mysql_session"
|
||||||
|
require "inspec/resources/nftables"
|
||||||
require "inspec/resources/nginx"
|
require "inspec/resources/nginx"
|
||||||
require "inspec/resources/nginx_conf"
|
require "inspec/resources/nginx_conf"
|
||||||
require "inspec/resources/npm"
|
require "inspec/resources/npm"
|
||||||
|
|
251
lib/inspec/resources/nftables.rb
Normal file
251
lib/inspec/resources/nftables.rb
Normal file
|
@ -0,0 +1,251 @@
|
||||||
|
require "inspec/resources/command"
|
||||||
|
require "json" unless defined?(JSON)
|
||||||
|
|
||||||
|
# @see https://wiki.nftables.org/
|
||||||
|
# @see https://www.netfilter.org/projects/nftables/manpage.html
|
||||||
|
|
||||||
|
# rubocop:disable Style/ClassVars
|
||||||
|
|
||||||
|
module Inspec::Resources
|
||||||
|
class NfTables < Inspec.resource(1)
|
||||||
|
name "nftables"
|
||||||
|
supports platform: "linux"
|
||||||
|
desc "Use the nftables InSpec audit resource to test rules and sets that are defined in nftables, which maintains tables of IP packet filtering rules. There may be more than one table. Each table contains one (or more) chains. A chain is a list of rules that match packets. When the rule matches, the rule defines what target to assign to the packet."
|
||||||
|
example <<~EXAMPLE
|
||||||
|
describe nftables(family:'inet', table:'filter', chain: 'INPUT') do
|
||||||
|
its('type') { should eq 'filter' }
|
||||||
|
its('hook') { should eq 'input' }
|
||||||
|
its('prio') { should eq 0 } # filter
|
||||||
|
its('policy') { should eq 'drop' }
|
||||||
|
it { should have_rule('tcp dport { 22, 80, 443 } accept') }
|
||||||
|
end
|
||||||
|
describe nftables(family: 'inet', table: 'filter', set: 'OPEN_PORTS') do
|
||||||
|
its('type') { should eq 'ipv4_addr . inet_proto . inet_service' }
|
||||||
|
its('flags') { should include 'interval' }
|
||||||
|
it { should have_element('1.1.1.1 . tcp . 25-27') }
|
||||||
|
end
|
||||||
|
EXAMPLE
|
||||||
|
|
||||||
|
@@bin = nil
|
||||||
|
@@nft_params = {}
|
||||||
|
@@nft_params["json"] = ""
|
||||||
|
@@nft_params["stateless"] = ""
|
||||||
|
@@nft_params["num"] = ""
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# Let's have a generic method to retrieve attributes for chains and sets
|
||||||
|
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
|
||||||
|
|
||||||
|
# Create a method for each attribute
|
||||||
|
%i{flags hook policy prio size type}.each do |attr_method|
|
||||||
|
define_method attr_method do
|
||||||
|
_get_attr(attr_method.to_s)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
@nftables_cache[idx] = cmd.stdout.gsub("\t", "").split("\n").reject { |line| line =~ /^(table|set|type|size|flags|typeof|auto-merge)/ || line =~ /^}$/ }.map { |line| line.sub("elements = {", "").sub("}", "").split(",") }.flatten.map(&:strip)
|
||||||
|
end
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
def resource_id
|
||||||
|
to_s || "nftables"
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_s
|
||||||
|
format("nftables (%s %s %s %s)", @family && "family: #{@family}", @table && "table: #{@table}", @chain && "chain: #{@chain}", @set && "set: #{@set}").strip
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def remove_comments_from_rules(rules)
|
||||||
|
rules.each do |rule|
|
||||||
|
next if rule.nil?
|
||||||
|
|
||||||
|
rule.gsub!(/ comment "([^"]*)"/, "")
|
||||||
|
rule.strip
|
||||||
|
end
|
||||||
|
rules
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_nftables_or_error
|
||||||
|
%w{/usr/sbin/nft /sbin/nft nft}.each do |cmd|
|
||||||
|
return cmd if inspec.command(cmd).exist?
|
||||||
|
end
|
||||||
|
|
||||||
|
raise Inspec::Exceptions::ResourceFailed, "Could not find `nft`"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -148,7 +148,7 @@ RSpec::Matchers.define :be_resolvable do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# matcher for iptables and ip6tables
|
# matcher for iptables, ip6tables and nftables
|
||||||
RSpec::Matchers.define :have_rule do |rule|
|
RSpec::Matchers.define :have_rule do |rule|
|
||||||
match do |tables|
|
match do |tables|
|
||||||
tables.has_rule?(rule)
|
tables.has_rule?(rule)
|
||||||
|
@ -163,6 +163,13 @@ RSpec::Matchers.define :have_rule do |rule|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# matcher for nftables sets
|
||||||
|
RSpec::Matchers.define :have_element do |elem|
|
||||||
|
match do |sets|
|
||||||
|
sets.has_element?(elem)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# `be_in` matcher
|
# `be_in` matcher
|
||||||
# You can use it in the following cases:
|
# You can use it in the following cases:
|
||||||
# - check if an item or array is included in a given array
|
# - check if an item or array is included in a given array
|
||||||
|
|
7
test/fixtures/cmd/nftables-chain
vendored
Normal file
7
test/fixtures/cmd/nftables-chain
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
table inet filter {
|
||||||
|
chain INPUT {
|
||||||
|
type filter hook input priority 0; policy accept;
|
||||||
|
iifname "eth0" tcp dport 80 accept comment "http on 80"
|
||||||
|
jump derby-cognos-web
|
||||||
|
}
|
||||||
|
}
|
1
test/fixtures/cmd/nftables-chain-json
vendored
Normal file
1
test/fixtures/cmd/nftables-chain-json
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{"nftables": [{"metainfo": {"version": "1.0.2", "release_name": "Lester Gooch", "json_schema_version": 1}}, {"chain": {"family": "inet", "table": "filter", "name": "INPUT", "handle": 1, "type": "filter", "hook": "input", "prio": 0, "policy": "accept"}}, {"rule": {"family": "inet", "table": "filter", "chain": "INPUT", "handle": 4, "comment": "http on 80", "expr": [{"match": {"op": "==", "left": {"meta": {"key": "iifname"}}, "right": "eth0"}}, {"match": {"op": "==", "left": {"payload": {"protocol": "tcp", "field": "dport"}}, "right": 80}}, {"accept": null}]}}, {"rule": {"family": "inet", "table": "filter", "chain": "INPUT", "handle": 5, "expr": [{"jump": {"target": "derby-cognos-web"}}]}}]}
|
8
test/fixtures/cmd/nftables-set
vendored
Normal file
8
test/fixtures/cmd/nftables-set
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
table inet filter {
|
||||||
|
set OPEN_PORTS {
|
||||||
|
type ipv4_addr
|
||||||
|
size 65536
|
||||||
|
flags interval
|
||||||
|
elements = { 1.1.1.1 }
|
||||||
|
}
|
||||||
|
}
|
1
test/fixtures/cmd/nftables-set-json
vendored
Normal file
1
test/fixtures/cmd/nftables-set-json
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{"nftables": [{"metainfo": {"version": "1.0.2", "release_name": "Lester Gooch", "json_schema_version": 1}}, {"set": {"family": "inet", "name": "OPEN_PORTS", "table": "filter", "type": "ipv4_addr", "handle": 3, "size": 65536, "flags": ["interval"], "elem": ["1.1.1.1"]}}]}
|
1
test/fixtures/cmd/nftables-version
vendored
Normal file
1
test/fixtures/cmd/nftables-version
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
nftables v1.0.2 (Lester Gooch)
|
|
@ -385,6 +385,24 @@ class MockLoader
|
||||||
# ip6tables
|
# ip6tables
|
||||||
"/usr/sbin/ip6tables -S" => cmd.call("ip6tables-s"),
|
"/usr/sbin/ip6tables -S" => cmd.call("ip6tables-s"),
|
||||||
%{sh -c 'type "/usr/sbin/ip6tables"'} => empty.call,
|
%{sh -c 'type "/usr/sbin/ip6tables"'} => empty.call,
|
||||||
|
# nftables (version)
|
||||||
|
"/usr/sbin/nft --version" => cmd.call("nftables-version"),
|
||||||
|
# nftables (chain with json output)
|
||||||
|
"/usr/sbin/nft -s -j list chain inet filter INPUT" => cmd.call("nftables-chain-json"),
|
||||||
|
"/usr/sbin/nft -j list chain inet filter INPUT" => cmd.call("nftables-chain-json"),
|
||||||
|
# nftables (chain)
|
||||||
|
"/usr/sbin/nft -s list chain inet filter INPUT" => cmd.call("nftables-chain"),
|
||||||
|
"/usr/sbin/nft -s -y list chain inet filter INPUT" => cmd.call("nftables-chain"),
|
||||||
|
"/usr/sbin/nft -s -nn list chain inet filter INPUT" => cmd.call("nftables-chain"),
|
||||||
|
"/usr/sbin/nft -y list chain inet filter INPUT" => cmd.call("nftables-chain"),
|
||||||
|
"/usr/sbin/nft list chain inet filter INPUT" => cmd.call("nftables-chain"),
|
||||||
|
# nftables (set with json output)
|
||||||
|
"/usr/sbin/nft -s -j list set inet filter OPEN_PORTS" => cmd.call("nftables-set-json"),
|
||||||
|
"/usr/sbin/nft -j list set inet filter OPEN_PORTS" => cmd.call("nftables-set-json"),
|
||||||
|
# nftables (set)
|
||||||
|
"/usr/sbin/nft -s list set inet filter OPEN_PORTS" => cmd.call("nftables-set"),
|
||||||
|
"/usr/sbin/nft list set inet filter OPEN_PORTS" => cmd.call("nftables-set"),
|
||||||
|
%{sh -c 'type "/usr/sbin/nft"'} => empty.call,
|
||||||
# ipnat
|
# ipnat
|
||||||
"/usr/sbin/ipnat -l" => cmd.call("ipnat-l"),
|
"/usr/sbin/ipnat -l" => cmd.call("ipnat-l"),
|
||||||
%{type "/usr/sbin/ipnat"} => empty.call,
|
%{type "/usr/sbin/ipnat"} => empty.call,
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
default["osprepare"]["docker"] = false
|
default["osprepare"]["docker"] = false
|
||||||
default["osprepare"]["application"] = true
|
default["osprepare"]["application"] = true
|
||||||
|
default["osprepare"]["nftables"] = false
|
||||||
|
|
|
@ -26,7 +26,11 @@ include_recipe("os_prepare::service")
|
||||||
include_recipe("os_prepare::package")
|
include_recipe("os_prepare::package")
|
||||||
include_recipe("os_prepare::registry_key")
|
include_recipe("os_prepare::registry_key")
|
||||||
include_recipe("os_prepare::iis")
|
include_recipe("os_prepare::iis")
|
||||||
include_recipe("os_prepare::iptables")
|
if node["osprepare"]["nftables"]
|
||||||
|
include_recipe("os_prepare::nftables")
|
||||||
|
else
|
||||||
|
include_recipe("os_prepare::iptables")
|
||||||
|
end
|
||||||
include_recipe("os_prepare::x509")
|
include_recipe("os_prepare::x509")
|
||||||
include_recipe("os_prepare::dh_params")
|
include_recipe("os_prepare::dh_params")
|
||||||
|
|
||||||
|
|
12
test/kitchen/cookbooks/os_prepare/recipes/nftables.rb
Normal file
12
test/kitchen/cookbooks/os_prepare/recipes/nftables.rb
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
if platform_family?("rhel", "debian", "fedora", "suse")
|
||||||
|
package "nftables"
|
||||||
|
execute "nft flush ruleset"
|
||||||
|
execute "nft add table inet filter"
|
||||||
|
execute 'nft add chain inet filter INPUT \{ type filter hook input priority 0\; policy accept\; \}'
|
||||||
|
execute "nft add chain inet filter derby-cognos-web"
|
||||||
|
execute 'nft add set inet filter OPEN_PORTS \{ type ipv4_addr\; size 65536\; flags interval\; \}'
|
||||||
|
execute 'nft add rule inet filter INPUT iifname eth0 tcp dport 80 accept comment \"http on 80\"'
|
||||||
|
execute "nft add rule inet filter INPUT jump derby-cognos-web"
|
||||||
|
execute 'nft add rule inet filter derby-cognos-web tcp dport 80 accept comment "derby-cognos-web"'
|
||||||
|
execute 'nft add element inet filter OPEN_PORTS \{ 1.1.1.1/32 \}'
|
||||||
|
end
|
|
@ -2,6 +2,10 @@ unless ENV['IPV6']
|
||||||
$stderr.puts "\033[1;33mTODO: Not running #{__FILE__.split("/").last} because we are running without IPv6\033[0m"
|
$stderr.puts "\033[1;33mTODO: Not running #{__FILE__.split("/").last} because we are running without IPv6\033[0m"
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
if ENV['NFTABLES']
|
||||||
|
$stderr.puts "\033[1;33mTODO: Not running #{__FILE__.split("/").last} because we are running with nftables\033[0m"
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
case os[:family]
|
case os[:family]
|
||||||
when 'ubuntu', 'fedora', 'debian', 'suse'
|
when 'ubuntu', 'fedora', 'debian', 'suse'
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
|
if ENV['NFTABLES']
|
||||||
|
$stderr.puts "\033[1;33mTODO: Not running #{__FILE__.split("/").last} because we are running with nftables\033[0m"
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
case os[:family]
|
case os[:family]
|
||||||
when 'ubuntu', 'fedora', 'debian', 'suse'
|
when 'ubuntu', 'fedora', 'debian', 'suse'
|
||||||
describe iptables do
|
describe iptables do
|
||||||
|
|
23
test/kitchen/policies/default/controls/nftables_spec.rb
Normal file
23
test/kitchen/policies/default/controls/nftables_spec.rb
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
unless ENV['NFTABLES']
|
||||||
|
$stderr.puts "\033[1;33mTODO: Not running #{__FILE__.split("/").last} because we are running with iptables\033[0m"
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
case os[:family]
|
||||||
|
when 'ubuntu', 'fedora', 'debian', 'suse', 'redhat', 'centos'
|
||||||
|
describe nftables(family: 'inet', table: 'filter', chain: 'INPUT') do
|
||||||
|
its('type') { should eq 'filter' }
|
||||||
|
its('hook') { should eq 'input' }
|
||||||
|
its('prio') { should eq 0 }
|
||||||
|
its('policy') { should eq 'accept' }
|
||||||
|
it { should have_rule('iifname "eth0" tcp dport 80 accept comment "http on 80"') }
|
||||||
|
it { should_not have_rule('iifname "eth1" tcp dport 80 accept') }
|
||||||
|
end
|
||||||
|
|
||||||
|
describe nftables(family: 'inet', table: 'filter', set: 'OPEN_PORTS') do
|
||||||
|
its('type') { should eq 'ipv4_addr' }
|
||||||
|
its('flags') { should include 'interval' }
|
||||||
|
it { should have_element('1.1.1.1') }
|
||||||
|
it { should_not have_element('2.2.2.2') }
|
||||||
|
end
|
||||||
|
end
|
27
test/unit/resources/nftables_test.rb
Normal file
27
test/unit/resources/nftables_test.rb
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
require "helper"
|
||||||
|
require "inspec/resource"
|
||||||
|
require "inspec/resources/nftables"
|
||||||
|
|
||||||
|
describe "Inspec::Resources::NfTables" do
|
||||||
|
|
||||||
|
# ubuntu
|
||||||
|
it "verify nftables chain on ubuntu" do
|
||||||
|
resource = MockLoader.new(:ubuntu).load_resource("nftables", { family: "inet", table: "filter", chain: "INPUT" })
|
||||||
|
_(resource.type).must_equal "filter"
|
||||||
|
_(resource.hook).must_equal "input"
|
||||||
|
_(resource.prio).must_equal 0
|
||||||
|
_(resource.policy).must_equal "accept"
|
||||||
|
_(resource.has_rule?('iifname "eth0" tcp dport 80 accept comment "http on 80"')).must_equal true
|
||||||
|
_(resource.has_rule?('iifname "eth1" tcp dport 80 accept')).must_equal false
|
||||||
|
_(resource.resource_id).must_equal "nftables (family: inet table: filter chain: INPUT )"
|
||||||
|
end
|
||||||
|
it "verify nftables set on ubuntu" do
|
||||||
|
resource = MockLoader.new(:ubuntu).load_resource("nftables", { family: "inet", table: "filter", set: "OPEN_PORTS" })
|
||||||
|
_(resource.type).must_equal "ipv4_addr"
|
||||||
|
_(resource.flags).must_include "interval"
|
||||||
|
_(resource.size).must_equal 65536
|
||||||
|
_(resource.has_element?("1.1.1.1")).must_equal true
|
||||||
|
_(resource.has_element?("2.2.2.2")).must_equal false
|
||||||
|
_(resource.resource_id).must_equal "nftables (family: inet table: filter set: OPEN_PORTS)"
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in a new issue