diff --git a/docs/resources.rst b/docs/resources.rst index 3af44ec12..f5237b2c4 100644 --- a/docs/resources.rst +++ b/docs/resources.rst @@ -26,6 +26,7 @@ The following InSpec audit resources are available: * `kernel_parameter`_ * `limits_conf`_ * `login_defs`_ +* `mount`_ * `mysql_conf`_ * `mysql_session`_ * `npm`_ @@ -2116,6 +2117,84 @@ The following examples show how to use this InSpec audit resource. its('PASS_MAX_DAYS') { should eq '90' } end + +mount +===================================================== +Use the ``mount`` |inspec resource| to test the mountpoints on |linux| systems. + +**Stability: Experimental** + +Syntax +----------------------------------------------------- +An ``mount`` |inspec resource| block declares the synchronization settings that should be tested: + +.. code-block:: ruby + + describe mount('path') do + it { should MATCHER 'value' } + end + +where + +* ``('path')`` is the path to the mounted directory +* ``MATCHER`` is a valid matcher for this |inspec resource| +* ``'value'`` is the value to be tested + +Matchers +----------------------------------------------------- +This |inspec resource| has the following matchers: + +be_mounted ++++++++++++++++++++++++++++++++++++++++++++++++++++++ +The ``be_mounted`` matcher tests if the file is accessible from the file system: + +.. code-block:: ruby + + it { should be_mounted } + +device ++++++++++++++++++++++++++++++++++++++++++++++++++++++ +The ``device`` matcher tests the device from the fstab table: + +.. code-block:: ruby + + its('device') { should eq '/dev/mapper/VolGroup-lv_root' } + +type ++++++++++++++++++++++++++++++++++++++++++++++++++++++ +The ``type`` matcher tests the filesystem type: + +.. code-block:: ruby + + its('type') { should eq 'ext4' } + + +options ++++++++++++++++++++++++++++++++++++++++++++++++++++++ +The ``options`` matcher tests the mount options for the filesystem from the fstab table: + +.. code-block:: ruby + + its('options') { should eq ['rw', 'mode=620'] } + + +Examples +----------------------------------------------------- +The following examples show how to use this InSpec audit resource. + +**Test a the mount point on '/'** + +.. code-block:: ruby + + describe mount('/') do + it { should be_mounted } + its('device') { should eq '/dev/mapper/VolGroup-lv_root' } + its('type') { should eq 'ext4' } + its('options') { should eq ['rw', 'mode=620'] } + end + + + mysql_conf ===================================================== Use the ``mysql_conf`` |inspec resource| to test the contents of the configuration file for |mysql|, typically located at ``/etc/mysql/my.cnf`` or ``/etc/my.cnf``. diff --git a/inspec.gemspec b/inspec.gemspec index e8597b46b..9b223ecf8 100644 --- a/inspec.gemspec +++ b/inspec.gemspec @@ -24,7 +24,7 @@ Gem::Specification.new do |spec| spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) spec.require_paths = ['lib'] - spec.add_dependency 'r-train', '~> 0.9' + spec.add_dependency 'r-train', '~> 0.9', '>= 0.9.3' spec.add_dependency 'thor', '~> 0.19' spec.add_dependency 'json', '~> 1.8' spec.add_dependency 'rainbow', '~> 2' diff --git a/lib/inspec/resource.rb b/lib/inspec/resource.rb index be2a4a8d4..723c85a87 100644 --- a/lib/inspec/resource.rb +++ b/lib/inspec/resource.rb @@ -44,6 +44,7 @@ require 'resources/kernel_module' require 'resources/kernel_parameter' require 'resources/limits_conf' require 'resources/login_def' +require 'resources/mount' require 'resources/mysql' require 'resources/mysql_conf' require 'resources/mysql_session' diff --git a/lib/matchers/matchers.rb b/lib/matchers/matchers.rb index 7414d7c3b..3a898b003 100644 --- a/lib/matchers/matchers.rb +++ b/lib/matchers/matchers.rb @@ -271,3 +271,33 @@ RSpec::Matchers.define :cmp do |expected| "\nexpected: value != #{expected}\n got: #{actual}\n\n(compared using `cmp` matcher)\n" end end + +# user resource matcher for serverspec compatibility +# This matcher will be deprecated in future +RSpec::Matchers.define :be_mounted do + match do |path| + if !@options.nil? + path.mounted?(@options, @identical) + else + path.mounted? + end + end + + chain :with do |attr| + @options = attr + @identical = false + end + + chain :only_with do |attr| + @options = attr + @identical = true + end + + failure_message do |path| + if !@options.nil? + "\n#{path} is not mounted with the options\n expected: #{@options}\n got: #{path.mount_options}\n" + else + "\n#{path} is not mounted\n" + end + end +end diff --git a/lib/resources/etc_group.rb b/lib/resources/etc_group.rb index 4871884f8..10f6ac588 100644 --- a/lib/resources/etc_group.rb +++ b/lib/resources/etc_group.rb @@ -26,7 +26,7 @@ require 'utils/parser' class EtcGroup < Inspec.resource(1) include Converter - include ContentParser + include CommentParser name 'etc_group' desc 'Use the etc_group InSpec audit resource to test groups that are defined on Linux and UNIX platforms. The /etc/group file stores details about each group---group name, password, group identifier, along with a comma-separate list of users that belong to the group.' diff --git a/lib/resources/file.rb b/lib/resources/file.rb index 28cb0cb02..0e7687acc 100644 --- a/lib/resources/file.rb +++ b/lib/resources/file.rb @@ -5,7 +5,7 @@ # license: All rights reserved module Inspec::Resources - class File < Inspec.resource(1) + class File < Inspec.resource(1) # rubocop:disable Metrics/ClassLength name 'file' desc 'Use the file InSpec audit resource to test all system file types, including files, directories, symbolic links, named pipes, sockets, character devices, block devices, and doors.' example " @@ -18,8 +18,9 @@ module Inspec::Resources its('mode') { should eq 0644 } end " + include MountParser - attr_reader :file, :path + attr_reader :file, :path, :mount_options def initialize(path) @path = path @file = inspec.backend.file(@path) @@ -28,7 +29,7 @@ module Inspec::Resources %w{ type exist? file? block_device? character_device? socket? directory? symlink? pipe? mode mode? owner owned_by? group grouped_into? link_target - link_path linked_to? content mtime size selinux_label mounted? immutable? + link_path linked_to? content mtime size selinux_label immutable? product_version file_version version? md5sum sha256sum }.each do |m| define_method m.to_sym do |*args| @@ -58,6 +59,30 @@ module Inspec::Resources file_permission_granted?('x', by_usergroup, by_specific_user) end + def mounted?(expected_options = nil, identical = false) + mounted = file.mounted + + # return if no additional parameters have been provided + return file.mounted? if expected_options.nil? + + # deprecation warning, this functionality will be removed in future version + warn "[DEPRECATION] `be_mounted.with and be_mounted.only_with` are deprecated. Please use `mount('#{path}')` instead." + + # we cannot read mount data on non-Linux systems + return nil if !inspec.os.linux? + + # parse content if we are on linux + @mount_options ||= parse_mount_options(mounted.stdout, true) + + if identical + # check if the options should be identical + @mount_options == expected_options + else + # otherwise compare the selected values + @mount_options.contains(expected_options) + end + end + def to_s "File #{path}" end diff --git a/lib/resources/mount.rb b/lib/resources/mount.rb new file mode 100644 index 000000000..8be324ad5 --- /dev/null +++ b/lib/resources/mount.rb @@ -0,0 +1,57 @@ +# encoding: utf-8 +# author: Christoph Hartmann +# author: Dominik Richter + +require 'utils/simpleconfig' + +class Mount < Inspec.resource(1) + name 'mount' + desc 'Use the mount InSpec audit resource to test if mount points.' + example " + describe mount('/') do + it { should be_mounted } + its(:count) { should eq 1 } + its('device') { should eq '/dev/mapper/VolGroup-lv_root' } + its('type') { should eq 'ext4' } + its('options') { should eq ['rw', 'mode=620'] } + end + " + include MountParser + + attr_reader :file + + def initialize(path) + @path = path + return skip_resource 'The `mount` resource is not supported on your OS yet.' if !inspec.os.linux? + @file = inspec.backend.file(@path) + end + + def mounted? + file.mounted? + end + + def count + mounted = file.mounted + return nil if mounted.nil? || mounted.stdout.nil? + mounted.stdout.lines.count + end + + def method_missing(name) + return nil if !file.mounted? + + mounted = file.mounted + return nil if mounted.nil? || mounted.stdout.nil? + + line = mounted.stdout + # if we got multiple lines, only use the last entry + line = mounted.stdout.lines.to_a.last if mounted.stdout.lines.count > 1 + + # parse content if we are on linux + @mount_options ||= parse_mount_options(line) + @mount_options[name] + end + + def to_s + "Mount #{@path}" + end +end diff --git a/lib/resources/passwd.rb b/lib/resources/passwd.rb index be67646a0..aa66863d0 100644 --- a/lib/resources/passwd.rb +++ b/lib/resources/passwd.rb @@ -37,7 +37,7 @@ class Passwd < Inspec.resource(1) end " - include ContentParser + include PasswdParser attr_reader :uid attr_reader :parsed diff --git a/lib/resources/user.rb b/lib/resources/user.rb index 0947deac3..c7c3a2cbb 100644 --- a/lib/resources/user.rb +++ b/lib/resources/user.rb @@ -230,7 +230,8 @@ class UnixUser < UserInfo end class LinuxUser < UnixUser - include ContentParser + include PasswdParser + include CommentParser def meta_info(username) cmd = inspec.command("getent passwd #{username}") @@ -295,7 +296,7 @@ end # - chpass(1) A flexible tool for changing user database information. # - passwd(1) The command-line tool to change user passwords. class FreeBSDUser < UnixUser - include ContentParser + include PasswdParser def meta_info(username) cmd = inspec.command("pw usershow #{username} -7") diff --git a/lib/utils/hash.rb b/lib/utils/hash.rb index 532837833..1f01b022a 100644 --- a/lib/utils/hash.rb +++ b/lib/utils/hash.rb @@ -1,13 +1,41 @@ # encoding: utf-8 -# Inspired by: http://stackoverflow.com/a/9381776 # author: Dominik Richter # author: Christoph Hartmann class ::Hash + # Inspired by: http://stackoverflow.com/a/9381776 def deep_merge(second) merger = proc { |_key, v1, v2| v1.is_a?(Hash) && v2.is_a?(Hash) ? v1.merge(v2, &merger) : v2 } merge(second, &merger) end + + # converts a deep hash into a flat hash + # hash = { + # 'a' => 1, + # 'b' => {'c' => 2}, + # } + # hash.smash # => {"a"=>1, "b-c"=>2} + def smash(prefix = nil) + inject({}) do |acc, (key, value)| + index = prefix.to_s + key.to_s + if value.is_a?(Hash) + acc.merge(value.smash(index + '-')) + else + acc.merge(index => value) + end + end + end + + # deep check if all values are contained + def contains(contains) + hash = smash + contains = contains.smash + + contains.each do |key, val| + return false if hash[key] != val + end + true + end end diff --git a/lib/utils/parser.rb b/lib/utils/parser.rb index d786fa738..a02791552 100644 --- a/lib/utils/parser.rb +++ b/lib/utils/parser.rb @@ -2,7 +2,7 @@ # author: Christoph Hartmann # author: Dominik Richter -module ContentParser +module PasswdParser # Parse /etc/passwd files. # # @param [String] content the raw content of /etc/passwd @@ -29,7 +29,9 @@ module ContentParser 'shell' => x.at(6), } end +end +module CommentParser # Parse a line with a command. For example: `a = b # comment`. # Retrieves the actual content. # @@ -59,3 +61,34 @@ module ContentParser [line, idx_nl] end end + +module MountParser + # this parses the output of mount command (only tested on linux) + # this method expects only one line of the mount output + def parse_mount_options(mount_line, compatibility = false) + mount = mount_line.scan(/\S+/) + + # parse device and type + mount_options = { device: mount[0], type: mount[4] } + + if compatibility == false + # parse options as array + mount_options[:options] = mount[5].gsub(/\(|\)/, '').split(',') + else + # parse options as serverspec uses it, tbis is deprecated + mount_options[:options] = {} + mount[5].gsub(/\(|\)/, '').split(',').each do |option| + name, val = option.split('=') + if val.nil? + val = true + else + # parse numbers + val = val.to_i if val.match(/^\d+$/) + end + mount_options[:options][name.to_sym] = val + end + end + + mount_options + end +end diff --git a/lib/utils/simpleconfig.rb b/lib/utils/simpleconfig.rb index 9eddbfc8b..23d5339c8 100644 --- a/lib/utils/simpleconfig.rb +++ b/lib/utils/simpleconfig.rb @@ -7,7 +7,7 @@ require 'utils/parser' class SimpleConfig - include ContentParser + include CommentParser attr_reader :params, :groups def initialize(raw_data, opts = {}) diff --git a/test/helper.rb b/test/helper.rb index c4ee1e98b..60d8ab84b 100644 --- a/test/helper.rb +++ b/test/helper.rb @@ -196,7 +196,9 @@ class MockLoader # apache_conf 'find /etc/apache2/ports.conf -maxdepth 1 -type f' => cmd.call('find-apache2-ports-conf'), 'find /etc/apache2/conf-enabled/*.conf -maxdepth 1 -type f' => cmd.call('find-apache2-conf-enabled'), - + # mount + "mount | grep -- ' on /'" => cmd.call("mount"), + "mount | grep -- ' on /mnt/iso-disk'" => cmd.call("mount-multiple"), } @backend diff --git a/test/integration/cookbooks/os_prepare/recipes/default.rb b/test/integration/cookbooks/os_prepare/recipes/default.rb index 18ffdb723..a1afa035c 100644 --- a/test/integration/cookbooks/os_prepare/recipes/default.rb +++ b/test/integration/cookbooks/os_prepare/recipes/default.rb @@ -6,6 +6,7 @@ include_recipe('os_prepare::apt') include_recipe('os_prepare::file') +include_recipe('os_prepare::mount') include_recipe('os_prepare::json_yaml_csv_ini') include_recipe('os_prepare::package') include_recipe('os_prepare::registry_key') diff --git a/test/integration/cookbooks/os_prepare/recipes/mount.rb b/test/integration/cookbooks/os_prepare/recipes/mount.rb new file mode 100644 index 000000000..37c5bbe5d --- /dev/null +++ b/test/integration/cookbooks/os_prepare/recipes/mount.rb @@ -0,0 +1,29 @@ +# encoding: utf-8 +# author: Christoph Hartmann +# author: Dominik Richter +# +# file mount tests + +# download alpine linux for file mount tests +remote_file '/root/alpine-3.3.0-x86_64.iso' do + source 'http://wiki.alpinelinux.org/cgi-bin/dl.cgi/v3.3/releases/x86_64/alpine-3.3.0-x86_64.iso' + owner 'root' + group 'root' + mode '0755' + action :create +end + +# create mount directory +directory '/mnt/iso-disk' do + owner 'root' + group 'root' + mode '0755' + action :create +end + +# mount -o loop /root/alpine-3.3.0-x86_64.iso /mnt/iso-disk +mount '/mnt/iso-disk' do + device '/root/alpine-3.3.0-x86_64.iso' + options 'loop' + action [:mount, :enable] +end diff --git a/test/integration/test/integration/default/file_spec.rb b/test/integration/test/integration/default/file_spec.rb index f4fc26f40..a049e62b3 100644 --- a/test/integration/test/integration/default/file_spec.rb +++ b/test/integration/test/integration/default/file_spec.rb @@ -108,4 +108,33 @@ if os.unix? its('type') { should eq :directory } end + # for server spec compatibility + # Do not use `.with` or `.only_with`, this syntax is deprecated and will be removed + # in InSpec version 1 + describe file('/mnt/iso-disk') do + it { should be_mounted } + it { should be_mounted.with( :type => 'iso9660' ) } + it { should be_mounted.with( :type => 'iso9660', :options => { :ro => true } ) } + it { should be_mounted.with( :type => 'iso9660', :device => '/root/alpine-3.3.0-x86_64.iso' ) } + it { should_not be_mounted.with( :type => 'ext4' ) } + it { should_not be_mounted.with( :type => 'xfs' ) } + end + + # compare with exact match + # also see mount_spec.rb + describe file('/mnt/iso-disk') do + it { should be_mounted.only_with( { + :device=>"/root/alpine-3.3.0-x86_64.iso", + :type=>"iso9660", + :options=>{ + :ro=>true} + }) + } + end + +elsif os.windows? + describe file('C:\\Windows') do + it { should exist } + it { should be_directory } + end end diff --git a/test/integration/test/integration/default/mount_spec.rb b/test/integration/test/integration/default/mount_spec.rb new file mode 100644 index 000000000..353ed81d0 --- /dev/null +++ b/test/integration/test/integration/default/mount_spec.rb @@ -0,0 +1,10 @@ +# encoding: utf-8 + +# instead of `.with` or `.only_with` we recommend to use the `mount` resource +describe mount '/mnt/iso-disk' do + it { should be_mounted } + its('count') { should eq 1 } + its('device') { should eq '/root/alpine-3.3.0-x86_64.iso' } + its('type') { should eq 'iso9660' } + its('options') { should eq ['ro'] } +end diff --git a/test/unit/mock/cmd/mount b/test/unit/mock/cmd/mount new file mode 100644 index 000000000..abd2b379d --- /dev/null +++ b/test/unit/mock/cmd/mount @@ -0,0 +1 @@ +/dev/xvda1 on / type ext4 (rw,discard) diff --git a/test/unit/mock/cmd/mount-multiple b/test/unit/mock/cmd/mount-multiple new file mode 100644 index 000000000..64b4974f9 --- /dev/null +++ b/test/unit/mock/cmd/mount-multiple @@ -0,0 +1,2 @@ +/root/alpine-3.3.0-x86_64.iso on /mnt/iso-disk type iso9660 (ro) +/root/alpine-3.3.0-x86_64_2.iso on /mnt/iso-disk type iso9660 (ro) diff --git a/test/unit/resources/file_test.rb b/test/unit/resources/file_test.rb index b3efebb9a..59c53efc5 100644 --- a/test/unit/resources/file_test.rb +++ b/test/unit/resources/file_test.rb @@ -1,3 +1,7 @@ +# encoding: utf-8 +# author: Christoph Hartmann +# author: Dominik Richter + require 'helper' require 'inspec/resource' diff --git a/test/unit/resources/mount_test.rb b/test/unit/resources/mount_test.rb new file mode 100644 index 000000000..f04be55a3 --- /dev/null +++ b/test/unit/resources/mount_test.rb @@ -0,0 +1,26 @@ +# encoding: utf-8 +# author: Christoph Hartmann +# author: Dominik Richter + +require 'helper' +require 'inspec/resource' + +describe Inspec::Resources::File do + let(:root_resource) { load_resource('mount', '/') } + + it 'parses the mount data properly' do + root_resource.send(:device).must_equal('/dev/xvda1') + root_resource.send(:type).must_equal('ext4') + root_resource.send(:options).must_equal(['rw','discard']) + root_resource.send(:count).must_equal(1) + end + + let(:iso_resource) { load_resource('mount', '/mnt/iso-disk') } + + it 'parses the mount data properly' do + iso_resource.send(:device).must_equal('/root/alpine-3.3.0-x86_64_2.iso') + iso_resource.send(:type).must_equal('iso9660') + iso_resource.send(:options).must_equal(['ro']) + iso_resource.send(:count).must_equal(2) + end +end diff --git a/test/unit/utils/content_parser_test.rb b/test/unit/utils/passwd_parser_test.rb similarity index 87% rename from test/unit/utils/content_parser_test.rb rename to test/unit/utils/passwd_parser_test.rb index 61c6a021d..a3ea211aa 100644 --- a/test/unit/utils/content_parser_test.rb +++ b/test/unit/utils/passwd_parser_test.rb @@ -2,8 +2,8 @@ # author: Dominik Richter # author: Christoph Hartmann -describe ContentParser do - let (:parser) { Class.new() { include ContentParser }.new } +describe PasswdParser do + let (:parser) { Class.new() { include PasswdParser }.new } describe '#parse_passwd' do it 'parses nil content' do