diff --git a/docs/resources/etc_hosts_allow.md.erb b/docs/resources/etc_hosts_allow.md.erb new file mode 100644 index 000000000..46f12dce0 --- /dev/null +++ b/docs/resources/etc_hosts_allow.md.erb @@ -0,0 +1,64 @@ +--- +title: About the etc_hosts_allow Resource +--- + +# etc_hosts_allow + +Use the `etc_hosts_allow` InSpec audit resource to test rules set to accept daemon and client traffic set in /etc/hosts.allow file. + +## Syntax + +An etc/hosts.allow rule specifies one or more daemons mapped to one or more clients, +with zero or more options to use to accept traffic when found. + +## Syntax + +Use the where clause to match a property to one or more rules in the hosts.allow file. + + describe etc_hosts_allow.where { daemon == 'value' } do + its ('client_list') { should include ['values'] } + its ('options') { should include ['values'] } + end + +Use the optional constructor parameter to give an alternative path to hosts.allow + + describe etc_hosts_allow(hosts_path).where { daemon == 'value' } do + its ('client_list') { should include ['values'] } + its ('options') { should include ['values'] } + end + +where + +* `daemon` is a daemon that will be allowed to pass traffic in. +* `client_list` is a list of clients will be allowed to pass traffic in. +* `options` is a list of tasks that to be done with the rule when traffic is found. + +## Supported Properties + + 'daemon', 'client_list', 'options' + +## Property Examples and Return Types + +### daemon + +`daemon` returns a string containing the daemon that is allowed in the rule. + + describe etc_hosts_allow.where { client_list == ['127.0.1.154', '[:fff:fAb0::]'] } do + its('daemon') { should eq ['vsftpd', 'sshd'] } + end + +### client_list + +`client_list` returns a 2d string array where each entry contains the clients specified for the rule. + + describe etc_hosts_allow.where { daemon == 'sshd' } do + its('client_list') { should include ['192.168.0.0/16', '[abcd::0000:1234]'] } + end + +### options + +`options` returns a 2d string array where each entry contains any options specified for the rule. + + describe etc_hosts_allow.where { daemon == 'sshd' } do + its('options') { should include ['deny', 'echo "REJECTED"'] } + end diff --git a/docs/resources/etc_hosts_deny.md.erb b/docs/resources/etc_hosts_deny.md.erb new file mode 100644 index 000000000..2c819d4cd --- /dev/null +++ b/docs/resources/etc_hosts_deny.md.erb @@ -0,0 +1,64 @@ +--- +title: About the etc_hosts_deny Resource +--- + +# etc_hosts_deny + +Use the `etc_hosts_deny` InSpec audit resource to test rules set to reject daemon and client traffic set in /etc/hosts.deny. + +## Syntax + +An etc/hosts.deny rule specifies one or more daemons mapped to one or more clients, +with zero or more options to use to reject traffic when found. + +## Syntax + +Use the where clause to match a property to one or more rules in the hosts.deny file. + + describe etc_hosts_deny.where { daemon == 'value' } do + its ('client_list') { should include ['values'] } + its ('options') { should include ['values'] } + end + +Use the optional constructor parameter to give an alternative path to hosts.deny + + describe etc_hosts_deny(hosts_path).where { daemon == 'value' } do + its ('client_list') { should include ['values'] } + its ('options') { should include ['values'] } + end + +where + +* `daemon` is a daemon that will be rejected to pass traffic in. +* `client_list` is a list of clients will be rejected to pass traffic in. +* `options` is a list of tasks that to be done with the rule when traffic is found. + +## Supported Properties + + 'daemon', 'client_list', 'options' + +## Property Examples and Return Types + +### daemon + +`daemon` returns a string containing the daemon that is allowed in the rule. + + describe etc_hosts_deny.where { client_list == ['127.0.1.154', '[:fff:fAb0::]'] } do + its('daemon') { should eq ['vsftpd', 'sshd'] } + end + +### client_list + +`client_list` returns a 2d string array where each entry contains the clients specified for the rule. + + describe etc_hosts_deny.where { daemon == 'sshd' } do + its('client_list') { should include ['192.168.0.0/16', '[abcd::0000:1234]'] } + end + +### options + +`options` returns a 2d string array where each entry contains any options specified for the rule. + + describe etc_hosts_deny.where { daemon == 'sshd' } do + its('options') { should include ['deny', 'echo "REJECTED"'] } + end diff --git a/lib/inspec/resource.rb b/lib/inspec/resource.rb index 4c25ecdf9..836eebb67 100644 --- a/lib/inspec/resource.rb +++ b/lib/inspec/resource.rb @@ -92,6 +92,7 @@ require 'resources/docker_container' require 'resources/docker_image' require 'resources/etc_fstab' require 'resources/etc_group' +require 'resources/etc_hosts_allow_deny' require 'resources/etc_hosts' require 'resources/file' require 'resources/gem' diff --git a/lib/resources/etc_hosts_allow_deny.rb b/lib/resources/etc_hosts_allow_deny.rb new file mode 100644 index 000000000..cf51535e0 --- /dev/null +++ b/lib/resources/etc_hosts_allow_deny.rb @@ -0,0 +1,122 @@ +# encoding: utf-8 +# author: Matthew Dromazos + +require 'utils/parser' + +module Inspec::Resources + class EtcHostsAllow < Inspec.resource(1) + name 'etc_hosts_allow' + desc 'Use the etc_hosts_allow InSpec audit resource to test the connections + the client will allow. Controlled by the /etc/hosts.allow file.' + example " + describe etc_hosts_allow.where { daemon == 'ALL' } do + its('client_list') { should include ['127.0.0.1', '[::1]'] } + its('options') { should eq [[]] } + end + " + + attr_reader :params + + include CommentParser + + def initialize(hosts_allow_path = nil) + return skip_resource 'The `etc_hosts_allow` resource is not supported on your OS.' unless inspec.os.linux? + @conf_path = hosts_allow_path || '/etc/hosts.allow' + @content = nil + @params = nil + read_content + end + + filter = FilterTable.create + filter.add_accessor(:where) + .add_accessor(:entries) + .add(:daemon, field: 'daemon') + .add(:client_list, field: 'client_list') + .add(:options, field: 'options') + + filter.connect(self, :params) + + private + + def read_content + @content = '' + @params = {} + @content = split_daemons(read_file(@conf_path)) + @params = parse_conf(@content) + end + + def split_daemons(content) + split_daemons_list = [] + content.each do |line| + data, = parse_comment_line(line, comment_char: '#', standalone_comments: false) + next unless data != '' + data.split(':')[0].split(',').each do |daemon| + split_daemons_list.push("#{daemon} : " + line.split(':', 2)[1]) + end + end + split_daemons_list + end + + def parse_conf(content) + content.map do |line| + data, = parse_comment_line(line, comment_char: '#', standalone_comments: false) + parse_line(data) unless data == '' + end.compact + end + + def parse_line(line) + daemon, clients_and_options = line.split(/:\s+/, 2) + daemon = daemon.strip + + clients_and_options ||= '' + clients, options = clients_and_options.split(/\s+:\s+/, 2) + client_list = clients.split(/,/).map(&:strip) + + options ||= '' + options_list = options.split(/:\s+/).map(&:strip) + + { + 'daemon' => daemon, + 'client_list' => client_list, + 'options' => options_list, + } + end + + def read_file(conf_path = @conf_path) + # Determine if the file exists and contains anything, if not + # then access control is turned off. + file = inspec.file(conf_path) + if !file.file? + return skip_resource "Can't find file at \"#{@conf_path}\"" + end + raw_conf = file.content + if raw_conf.empty? && !file.empty? + return skip_resource("Unable to read file \"#{@conf_path}\"") + end + + # If there is a file and it contains content, continue + raw_conf.lines + end + end + + class EtcHostsDeny < EtcHostsAllow + name 'etc_hosts_deny' + desc 'Use the etc_hosts_deny InSpec audit resource to test the connections + the client will deny. Controlled by the /etc/hosts.deny file.' + example " + describe etc_hosts_deny.where { daemon_list == 'ALL' } do + its('client_list') { should eq [['127.0.0.1', '[::1]']] } + its('options') { should eq [] } + end + " + + def initialize(path = nil) + return skip_resource '`etc_hosts_deny` is not supported on your OS' unless inspec.os.linux? + super(path || '/etc/hosts.deny') + end + + def to_s + 'hosts.deny Configuration' + end + end +end diff --git a/test/helper.rb b/test/helper.rb index e366c8b62..0b345e140 100644 --- a/test/helper.rb +++ b/test/helper.rb @@ -170,6 +170,8 @@ class MockLoader '/var/lib/fake_rpmdb' => mockdir.call(true), '/var/lib/rpmdb_does_not_exist' => mockdir.call(false), '/etc/init/ssh.conf' => mockfile.call('upstart_ssh_enabled.conf'), + '/etc/hosts.allow' => mockfile.call('hosts.allow'), + '/etc/hosts.deny' => mockfile.call('hosts.deny'), } # create all mock commands diff --git a/test/unit/mock/files/hosts.allow b/test/unit/mock/files/hosts.allow new file mode 100644 index 000000000..4f067f79e --- /dev/null +++ b/test/unit/mock/files/hosts.allow @@ -0,0 +1,15 @@ +# +# hosts.allow This file contains access rules which are used to +# allow or deny connections to network services that +# either use the tcp_wrappers library or that have been +# started through a tcp_wrappers-enabled xinetd. +# +# See 'man 5 hosts_options' and 'man 5 hosts_access' +# for information on rule syntax. +# See 'man tcpd' for information on tcp_wrappers +# +# LOCALHOST (ALL TRAFFIC ALLOWED) DO NOT REMOVE FOLLOWING LINE +ALL: 127.0.0.1, [::1] +# Added for testing +LOCAL : [fe80::]/10 : deny +vsftpd, sshd : 127.0.1.154, [:fff:fAb0::] : deny : /etc/bin/ diff --git a/test/unit/mock/files/hosts.deny b/test/unit/mock/files/hosts.deny new file mode 100644 index 000000000..0cd2c5fed --- /dev/null +++ b/test/unit/mock/files/hosts.deny @@ -0,0 +1,17 @@ +# +# hosts.deny This file contains access rules which are used to +# allow or deny connections to network services that +# either use the tcp_wrappers library or that have been +# started through a tcp_wrappers-enabled xinetd. +# +# See 'man 5 hosts_options' and 'man 5 hosts_access' +# for information on rule syntax. +# See 'man tcpd' for information on tcp_wrappers +# +# LOCALHOST (ALL TRAFFIC ALLOWED) DO NOT REMOVE FOLLOWING LINE +ALL: 127.0.0.1, [::1] +# Allow SSH (you can limit this further using IP addresses - e.g. 192.168.0.*) +sshd: ALL +# Added for testing +LOCAL : [fe80::]/10 : deny +vsftpd , sshd : 127.0.1.154, [:fff:fAb0::] : deny : /etc/bin/ diff --git a/test/unit/resources/etc_hosts_allow_deny_test.rb b/test/unit/resources/etc_hosts_allow_deny_test.rb new file mode 100644 index 000000000..307f36aa4 --- /dev/null +++ b/test/unit/resources/etc_hosts_allow_deny_test.rb @@ -0,0 +1,73 @@ +# encoding: utf-8 +# author: Matthew Dromazos + +require 'helper' +require 'inspec/resource' + +describe 'Inspec::Resources::EtcHostsAllow' do + describe 'EtcHostsAllow Paramaters' do + resource = load_resource('etc_hosts_allow') + it 'Verify etc_hosts_allow filtering by `daemon`' do + entries = resource.where { daemon == 'ALL' } + _(entries.client_list).must_include ['127.0.0.1', '[::1]'] + _(entries.options).must_equal [[]] + end + it 'Verify etc_hosts_allow filtering by `client_list`' do + entries = resource.where { client_list == ['127.0.1.154', '[:fff:fAb0::]'] } + _(entries.daemon).must_equal ['vsftpd', 'sshd'] + _(entries.options).must_include ['deny', '/etc/bin/'] + end + it 'Verify etc_hosts_allow filtering by `options`' do + entries = resource.where { options == ['deny', '/etc/bin/'] } + _(entries.daemon).must_equal ['vsftpd', 'sshd'] + _(entries.client_list).must_include ['127.0.1.154', '[:fff:fAb0::]'] + end + end + + describe '#parse_line' do + resource = load_resource('etc_hosts_allow') + it 'parses a line with multiple clients' do + line = 'foo: client1, client2 : some_option' + attributes = resource.send(:parse_line, line) + _(attributes['daemon']).must_equal 'foo' + _(attributes['client_list']).must_equal ['client1', 'client2'] + end + + it 'parses a line with one option' do + line = 'foo: client1, client2 : some_option' + attributes = resource.send(:parse_line, line) + _(attributes['daemon']).must_equal 'foo' + _(attributes['client_list']).must_equal ['client1', 'client2'] + _(attributes['options']).must_equal ['some_option'] + end + + it 'parses a line with multiple options' do + line = 'foo: client1, client2 : some_option : other_option' + attributes = resource.send(:parse_line, line) + _(attributes['daemon']).must_equal 'foo' + _(attributes['client_list']).must_equal ['client1', 'client2'] + _(attributes['options']).must_equal ['some_option', 'other_option'] + end + end +end + +describe 'Inspec::Resources::EtcHostsDeny' do + describe 'EtcHostsDeny Paramaters' do + resource = load_resource('etc_hosts_deny') + it 'Verify etc_hosts_deny filtering by `daemon`' do + entries = resource.where { daemon == 'ALL' } + _(entries.client_list).must_include ['127.0.0.1', '[::1]'] + _(entries.options).must_equal [[]] + end + it 'Verify etc_hosts_deny filtering by `client_list`' do + entries = resource.where { client_list == ['127.0.1.154', '[:fff:fAb0::]'] } + _(entries.daemon).must_equal ['vsftpd', 'sshd'] + _(entries.options).must_include ['deny', '/etc/bin/'] + end + it 'Verify etc_hosts_deny filtering by `options`' do + entries = resource.where { options == ['deny', '/etc/bin/'] } + _(entries.daemon).must_equal ['vsftpd', 'sshd'] + _(entries.client_list).must_include ['127.0.1.154', '[:fff:fAb0::]'] + end + end +end