mirror of
https://github.com/inspec/inspec
synced 2024-09-20 22:41:55 +00:00
Merge pull request #341 from chef/chris-rock/mount
implement `mount` resource
This commit is contained in:
commit
5df44fef9a
22 changed files with 372 additions and 14 deletions
|
@ -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``.
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.'
|
||||
|
|
|
@ -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
|
||||
|
|
57
lib/resources/mount.rb
Normal file
57
lib/resources/mount.rb
Normal file
|
@ -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
|
|
@ -37,7 +37,7 @@ class Passwd < Inspec.resource(1)
|
|||
end
|
||||
"
|
||||
|
||||
include ContentParser
|
||||
include PasswdParser
|
||||
|
||||
attr_reader :uid
|
||||
attr_reader :parsed
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
require 'utils/parser'
|
||||
|
||||
class SimpleConfig
|
||||
include ContentParser
|
||||
include CommentParser
|
||||
|
||||
attr_reader :params, :groups
|
||||
def initialize(raw_data, opts = {})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
29
test/integration/cookbooks/os_prepare/recipes/mount.rb
Normal file
29
test/integration/cookbooks/os_prepare/recipes/mount.rb
Normal file
|
@ -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
|
|
@ -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
|
||||
|
|
10
test/integration/test/integration/default/mount_spec.rb
Normal file
10
test/integration/test/integration/default/mount_spec.rb
Normal file
|
@ -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
|
1
test/unit/mock/cmd/mount
Normal file
1
test/unit/mock/cmd/mount
Normal file
|
@ -0,0 +1 @@
|
|||
/dev/xvda1 on / type ext4 (rw,discard)
|
2
test/unit/mock/cmd/mount-multiple
Normal file
2
test/unit/mock/cmd/mount-multiple
Normal file
|
@ -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)
|
|
@ -1,3 +1,7 @@
|
|||
# encoding: utf-8
|
||||
# author: Christoph Hartmann
|
||||
# author: Dominik Richter
|
||||
|
||||
require 'helper'
|
||||
require 'inspec/resource'
|
||||
|
||||
|
|
26
test/unit/resources/mount_test.rb
Normal file
26
test/unit/resources/mount_test.rb
Normal file
|
@ -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
|
|
@ -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
|
Loading…
Reference in a new issue