From 3263d7662741f51de230fdc1657ffadf37c82240 Mon Sep 17 00:00:00 2001 From: Lance Albertson Date: Sat, 22 Jun 2019 17:35:32 -0700 Subject: [PATCH] Add ip6tables resource This should resolve #1011 which provides an ip6tables resource to test IPv6 iptables rules. This is essentially a copy of the iptables resource with a few renames. In addition, I've pulled in the integration tests for iptables into ip6tables and enabled it on docker so that it properly gets tested regularly. The test cookbook recipe has been updated to support all of the current platforms that are being tested. Signed-off-by: Lance Albertson --- docs/resources/ip6tables.md.erb | 74 +++++++++++++++++ lib/inspec/resources.rb | 1 + lib/inspec/resources/ip6tables.rb | 79 +++++++++++++++++++ lib/matchers/matchers.rb | 2 +- test/cookbooks/os_prepare/recipes/default.rb | 2 +- test/cookbooks/os_prepare/recipes/iptables.rb | 15 +++- test/helpers/mock_loader.rb | 3 + .../default/controls/ip6tables_spec.rb | 23 ++++++ .../default/controls/iptables_spec.rb | 9 +-- test/unit/mock/cmd/ip6tables-s | 6 ++ test/unit/resources/ip6tables_test.rb | 32 ++++++++ 11 files changed, 236 insertions(+), 10 deletions(-) create mode 100644 docs/resources/ip6tables.md.erb create mode 100644 lib/inspec/resources/ip6tables.rb create mode 100644 test/integration/default/controls/ip6tables_spec.rb create mode 100644 test/unit/mock/cmd/ip6tables-s create mode 100644 test/unit/resources/ip6tables_test.rb diff --git a/docs/resources/ip6tables.md.erb b/docs/resources/ip6tables.md.erb new file mode 100644 index 000000000..53ff3a6d5 --- /dev/null +++ b/docs/resources/ip6tables.md.erb @@ -0,0 +1,74 @@ +--- +title: About the ip6tables Resource +platform: linux +--- + +# ip6tables + +Use the `ip6tables` Chef InSpec audit resource to test rules that are defined in `ip6tables`, which maintains tables of IP packet filtering rules for IPv6. There may be more than one table. Each table contains one (or more) chains (both built-in and custom). A chain is a list of rules that match packets. When the rule matches, the rule defines what target to assign to the packet. + +
+ +## Availability + +### Installation + +This resource is distributed along with Chef InSpec itself. You can use it automatically. + +### Version + +This resource first became available in v4.6.9 of InSpec. + +## Syntax + +A `ip6tables` resource block declares tests for rules in IP tables: + + describe ip6tables(rule:'name', table:'name', chain: 'name') do + it { should have_rule('RULE') } + end + +where + +* `ip6tables()` may specify any combination of `rule`, `table`, or `chain` +* `rule:'name'` is the name of a rule that matches a set of packets +* `table:'name'` is the packet matching table against which the test is run +* `chain: 'name'` is the name of a user-defined chain or one of `ACCEPT`, `DROP`, `QUEUE`, or `RETURN` +* `have_rule('RULE')` tests that rule in the ip6tables list. This must match the entire line taken from `ip6tables -S CHAIN`. + +
+ +## Examples + +The following examples show how to use this Chef InSpec audit resource. + +### Test if the INPUT chain is in default ACCEPT mode + + describe ip6tables do + it { should have_rule('-P INPUT ACCEPT') } + end + +### Test if the INPUT chain from the mangle table is in ACCEPT mode + + describe ip6tables(table:'mangle', chain: 'INPUT') do + it { should have_rule('-P INPUT ACCEPT') } + end + +### Test if there is a rule allowing Postgres (5432/TCP) traffic + + describe ip6tables do + it { should have_rule('-A INPUT -p tcp -m tcp -m multiport --dports 5432 -m comment --comment "postgres" -j ACCEPT') } + end + +Note that the rule specification must exactly match what's in the output of `ip6tables -S INPUT`, which will depend on how you've built your rules. + +
+ +## Matchers + +For a full list of available matchers, please visit our [matchers page](https://www.inspec.io/docs/reference/matchers/). + +### have_rule + +The `have_rule` matcher tests the named rule against the information in the `ip6tables` file: + + it { should have_rule('RULE') } diff --git a/lib/inspec/resources.rb b/lib/inspec/resources.rb index 4f453f0d1..7c1cf9a61 100644 --- a/lib/inspec/resources.rb +++ b/lib/inspec/resources.rb @@ -56,6 +56,7 @@ require "inspec/resources/iis_app_pool" require "inspec/resources/iis_site" require "inspec/resources/inetd_conf" require "inspec/resources/interface" +require "inspec/resources/ip6tables" require "inspec/resources/iptables" require "inspec/resources/kernel_module" require "inspec/resources/kernel_parameter" diff --git a/lib/inspec/resources/ip6tables.rb b/lib/inspec/resources/ip6tables.rb new file mode 100644 index 000000000..755755d6c --- /dev/null +++ b/lib/inspec/resources/ip6tables.rb @@ -0,0 +1,79 @@ +require "inspec/resources/command" + +# Usage: +# describe ip6tables do +# it { should have_rule('-P INPUT ACCEPT') } +# end +# +# The following serverspec sytax is not implemented: +# describe ip6tables do +# it { should have_rule('-P INPUT ACCEPT').with_table('mangle').with_chain('INPUT') } +# end +# Please use the new sytax: +# describe ip6tables(table:'mangle', chain: 'input') do +# it { should have_rule('-P INPUT ACCEPT') } +# end +# +# Note: Docker containers normally do not have ip6tables installed +# +# @see http://ipset.netfilter.org/ip6tables.man.html +# @see http://ipset.netfilter.org/ip6tables.man.html +module Inspec::Resources + class Ip6Tables < Inspec.resource(1) + name "ip6tables" + supports platform: "linux" + desc "Use the ip6tables InSpec audit resource to test rules that are defined in ip6tables, which maintains tables of IP packet filtering rules. There may be more than one table. Each table contains one (or more) chains (both built-in and custom). 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 ip6tables do + it { should have_rule('-P INPUT ACCEPT') } + end + EXAMPLE + + def initialize(params = {}) + @table = params[:table] + @chain = params[:chain] + + # we're done if we are on linux + return if inspec.os.linux? + + # ensures, all calls are aborted for non-supported os + @ip6tables_cache = [] + skip_resource "The `ip6tables` resource is not supported on your OS yet." + end + + def has_rule?(rule = nil, _table = nil, _chain = nil) + # checks if the rule is part of the ruleset + # for now, we expect an exact match + retrieve_rules.any? { |line| line.casecmp(rule) == 0 } + end + + def retrieve_rules + return @ip6tables_cache if defined?(@ip6tables_cache) + + # construct ip6tables command to read all rules + bin = find_ip6tables_or_error + table_cmd = "-t #{@table}" if @table + ip6tables_cmd = format("%s %s -S %s", bin, table_cmd, @chain).strip + + cmd = inspec.command(ip6tables_cmd) + return [] if cmd.exit_status.to_i != 0 + + # split rules, returns array or rules + @ip6tables_cache = cmd.stdout.split("\n").map(&:strip) + end + + def to_s + format("Ip6tables %s %s", @table && "table: #{@table}", @chain && "chain: #{@chain}").strip + end + + private + + def find_ip6tables_or_error + %w{/usr/sbin/ip6tables /sbin/ip6tables ip6tables}.each do |cmd| + return cmd if inspec.command(cmd).exist? + end + + raise Inspec::Exceptions::ResourceFailed, "Could not find `ip6tables`" + end + end +end diff --git a/lib/matchers/matchers.rb b/lib/matchers/matchers.rb index 38cebd5d1..602b33164 100644 --- a/lib/matchers/matchers.rb +++ b/lib/matchers/matchers.rb @@ -147,7 +147,7 @@ RSpec::Matchers.define :be_resolvable do end end -# matcher for iptables +# matcher for iptables and ip6tables RSpec::Matchers.define :have_rule do |rule| match do |tables| tables.has_rule?(rule) diff --git a/test/cookbooks/os_prepare/recipes/default.rb b/test/cookbooks/os_prepare/recipes/default.rb index fcd3a9991..d4d7336fb 100644 --- a/test/cookbooks/os_prepare/recipes/default.rb +++ b/test/cookbooks/os_prepare/recipes/default.rb @@ -37,7 +37,7 @@ include_recipe("os_prepare::service") include_recipe("os_prepare::package") include_recipe("os_prepare::registry_key") include_recipe("os_prepare::iis") -include_recipe("os_prepare::iptables") unless node["osprepare"]["docker"] +include_recipe("os_prepare::iptables") include_recipe("os_prepare::x509") include_recipe("os_prepare::dh_params") diff --git a/test/cookbooks/os_prepare/recipes/iptables.rb b/test/cookbooks/os_prepare/recipes/iptables.rb index 3d42608da..48f356b7b 100644 --- a/test/cookbooks/os_prepare/recipes/iptables.rb +++ b/test/cookbooks/os_prepare/recipes/iptables.rb @@ -1,4 +1,9 @@ -if platform_family?("rhel", "debian", "fedora") +if platform_family?("rhel", "debian", "fedora", "amazon", "suse") + package value_for_platform_family( + [ "centos", "oracle"] => [ "iptables", "iptables-ipv6" ], + "default" => [ "iptables" ] + ) + # IPv4 execute "iptables -A INPUT -i eth0 -p tcp -m tcp "\ "--dport 80 -m state --state NEW -m comment "\ '--comment "http on 80" -j ACCEPT' @@ -6,4 +11,12 @@ if platform_family?("rhel", "debian", "fedora") execute "iptables -A INPUT -j derby-cognos-web" execute "iptables -A derby-cognos-web -p tcp -m tcp --dport 80 "\ '-m comment --comment "derby-cognos-web" -j ACCEPT' + # IPv6 + execute "ip6tables -A INPUT -i eth0 -p tcp -m tcp "\ + "--dport 80 -m state --state NEW -m comment "\ + '--comment "http v6 on 80" -j ACCEPT' + execute "ip6tables -N derby-cognos-web-v6" + execute "ip6tables -A INPUT -j derby-cognos-web-v6" + execute "ip6tables -A derby-cognos-web-v6 -p tcp -m tcp --dport 80 "\ + '-m comment --comment "derby-cognos-web-v6" -j ACCEPT' end diff --git a/test/helpers/mock_loader.rb b/test/helpers/mock_loader.rb index 701c9d822..937f6ecc2 100644 --- a/test/helpers/mock_loader.rb +++ b/test/helpers/mock_loader.rb @@ -323,6 +323,9 @@ class MockLoader # iptables "/usr/sbin/iptables -S" => cmd.call("iptables-s"), %{bash -c 'type "/usr/sbin/iptables"'} => empty.call, + # ip6tables + "/usr/sbin/ip6tables -S" => cmd.call("ip6tables-s"), + %{bash -c 'type "/usr/sbin/ip6tables"'} => empty.call, # apache_conf "sh -c 'find /etc/apache2/ports.conf -type f -maxdepth 1'" => cmd.call("find-apache2-ports-conf"), "sh -c 'find /etc/httpd/conf.d/*.conf -type f -maxdepth 1'" => cmd.call("find-httpd-ssl-conf"), diff --git a/test/integration/default/controls/ip6tables_spec.rb b/test/integration/default/controls/ip6tables_spec.rb new file mode 100644 index 000000000..a955c4a99 --- /dev/null +++ b/test/integration/default/controls/ip6tables_spec.rb @@ -0,0 +1,23 @@ +case os[:family] +when 'ubuntu', 'fedora', 'debian', 'suse' + describe ip6tables do + it { should have_rule('-A INPUT -i eth0 -p tcp -m tcp --dport 80 -m state --state NEW -m comment --comment "http v6 on 80" -j ACCEPT') } + it { should_not have_rule('-A INPUT -i eth1 -p tcp -m tcp --dport 80 -j ACCEPT') } + + # single-word comments have their quotes dropped + it { should have_rule('-A derby-cognos-web-v6 -p tcp -m tcp --dport 80 -m comment --comment derby-cognos-web-v6 -j ACCEPT') } + end +when 'redhat', 'centos' + describe ip6tables do + it { should have_rule('-A INPUT -i eth0 -p tcp -m tcp --dport 80 -m state --state NEW -m comment --comment "http v6 on 80" -j ACCEPT') } + it { should_not have_rule('-A INPUT -i eth1 -p tcp -m tcp --dport 80 -j ACCEPT') } + end + + describe ip6tables do + it { should have_rule('-A derby-cognos-web-v6 -p tcp -m tcp --dport 80 -m comment --comment "derby-cognos-web-v6" -j ACCEPT') } + end if os[:release] == 6 + + describe ip6tables do + it { should have_rule('-A derby-cognos-web-v6 -p tcp -m tcp --dport 80 -m comment --comment derby-cognos-web-v6 -j ACCEPT') } + end if os[:release] == 7 +end diff --git a/test/integration/default/controls/iptables_spec.rb b/test/integration/default/controls/iptables_spec.rb index 974f14a2f..a79f9048e 100644 --- a/test/integration/default/controls/iptables_spec.rb +++ b/test/integration/default/controls/iptables_spec.rb @@ -1,10 +1,5 @@ -if ENV['DOCKER'] - $stderr.puts "\033[1;33mTODO: Not running #{__FILE__.split("/").last} because we are running in docker\033[0m" - return -end - case os[:family] -when 'ubuntu', 'fedora' +when 'ubuntu', 'fedora', 'debian', 'suse' describe iptables do it { should have_rule('-A INPUT -i eth0 -p tcp -m tcp --dport 80 -m state --state NEW -m comment --comment "http on 80" -j ACCEPT') } it { should_not have_rule('-A INPUT -i eth1 -p tcp -m tcp --dport 80 -j ACCEPT') } @@ -12,7 +7,7 @@ when 'ubuntu', 'fedora' # single-word comments have their quotes dropped it { should have_rule('-A derby-cognos-web -p tcp -m tcp --dport 80 -m comment --comment derby-cognos-web -j ACCEPT') } end -when 'rhel', 'centos' +when 'redhat', 'centos' describe iptables do it { should have_rule('-A INPUT -i eth0 -p tcp -m tcp --dport 80 -m state --state NEW -m comment --comment "http on 80" -j ACCEPT') } it { should_not have_rule('-A INPUT -i eth1 -p tcp -m tcp --dport 80 -j ACCEPT') } diff --git a/test/unit/mock/cmd/ip6tables-s b/test/unit/mock/cmd/ip6tables-s new file mode 100644 index 000000000..93f4718a5 --- /dev/null +++ b/test/unit/mock/cmd/ip6tables-s @@ -0,0 +1,6 @@ +-P INPUT DROP +-P FORWARD DROP +-P OUTPUT ACCEPT +-A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT +-A INPUT -i eth0 -p tcp -m tcp --dport 22 -m state --state NEW -j ACCEPT +-A INPUT -i eth0 -p tcp -m tcp --dport 80 -m state --state NEW -m comment --comment "http-v6 like its 1990" -j ACCEPT diff --git a/test/unit/resources/ip6tables_test.rb b/test/unit/resources/ip6tables_test.rb new file mode 100644 index 000000000..f070e3b90 --- /dev/null +++ b/test/unit/resources/ip6tables_test.rb @@ -0,0 +1,32 @@ +require "helper" +require "inspec/resource" +require "inspec/resources/ip6tables" + +describe "Inspec::Resources::Ip6tables" do + + # ubuntu 14.04 + it "verify ip6tables on ubuntu" do + resource = MockLoader.new(:ubuntu1404).load_resource("ip6tables") + _(resource.has_rule?("-P OUTPUT ACCEPT")).must_equal true + _(resource.has_rule?("-P OUTPUT DROP")).must_equal false + end + + it "verify ip6tables with comments on ubuntu" do + resource = MockLoader.new(:ubuntu1404).load_resource("ip6tables") + _(resource.has_rule?('-A INPUT -i eth0 -p tcp -m tcp --dport 80 -m state --state NEW -m comment --comment "http-v6 like its 1990" -j ACCEPT')).must_equal true + end + + it "verify ip6tables on windows" do + resource = MockLoader.new(:windows).load_resource("ip6tables") + _(resource.has_rule?("-P OUTPUT ACCEPT")).must_equal false + _(resource.has_rule?("-P OUTPUT DROP")).must_equal false + end + + # undefined + it "verify ip6tables on unsupported os" do + resource = MockLoader.new(:undefined).load_resource("ip6tables") + _(resource.has_rule?("-P OUTPUT ACCEPT")).must_equal false + _(resource.has_rule?("-P OUTPUT DROP")).must_equal false + end + +end