From a2a86860d6068c30bd696e1f97c715bf9d71c9e8 Mon Sep 17 00:00:00 2001 From: Aaron Lippold Date: Mon, 18 Mar 2019 10:49:06 -0400 Subject: [PATCH] This adds the `more_permissive_than?(mode)` matcher to the `file` resource. Fixes #3893 Signed-off-by: Aaron Lippold --- docs/resources/file.md.erb | 20 +++++++++++++++++++ lib/resources/file.rb | 33 ++++++++++++++++++++++++++++++++ test/unit/resources/file_test.rb | 14 ++++++++++++++ 3 files changed, 67 insertions(+) diff --git a/docs/resources/file.md.erb b/docs/resources/file.md.erb index 373f9c210..115b13000 100644 --- a/docs/resources/file.md.erb +++ b/docs/resources/file.md.erb @@ -119,6 +119,8 @@ not Without the zero prefix for the octal value, InSpec will interpret it as the _decimal_ value 644, which is octal 1024 or `-----w-r-T`, and any test for a file that is `-rw-r--r--` will fail. +#### Note: If you want to validate that a file has an upper or lower bound in `mode` see the [`more_permissive_than?`(mode)](#more_permissive_than?(mode)) matcher below. + ### mtime The `mtime` property tests if the file modification time for the file matches the specified value. The mtime, where supported, is returned as the number of seconds since the epoch. @@ -541,3 +543,21 @@ The `exist` matcher tests if the named file exists: The `have_mode` matcher tests if a file has a mode assigned to it: it { should have_mode } + +### `more_permissive_than?(mode)` + +`more_permissive_than?(mode)` takes the maximum desired mode - in `octal format` +('0644' or '0777') - of your file as a `String` and returns a `Boolean`. It will +return `true` if your file has a mode with greater permissions than specified. + + describe file('/etc/passwd') do + it { should_not be_more_permissive_than?('0644') } + it { should be_more_permissive_than?('0000') } + end + + my_file = file('/etc/passwd') + describe "The file #{my_file.path} with permissions: `#{my_file.mode.to_s(8)}`" do + subject { my_file } + it { should_not be_more_permissive_than?('0644') } + it { should be_more_permissive_than?('0000') } + end diff --git a/lib/resources/file.rb b/lib/resources/file.rb index 5b64de7aa..4dd737026 100644 --- a/lib/resources/file.rb +++ b/lib/resources/file.rb @@ -133,6 +133,35 @@ module Inspec::Resources alias sticky? sticky + def more_permissive_than?(max_mode = nil) + raise Inspec::Exceptions::ResourceFailed, 'The file' + file.path + 'doesn\'t seem to exist' unless exist? + raise ArgumentError, 'You must provide a value for the `maximum permission target` for the file.' if max_mode.nil? + raise ArgumentError, 'You must proivde the `maximum permission target` as a `String`, you provided: ' + max_mode.class.to_s unless max_mode.is_a?(String) + raise ArgumentError, 'The value of the `maximum permission target` should be a valid file mode in 4-ditgit octal format: for example, `0644` or `0777`' unless /(0)?([0-7])([0-7])([0-7])/.match?(max_mode) + + # Using the files mode and a few bit-wise calculations we can ensure a + # file is no more permisive than desired. + # + # 1. Calculate the inverse of the desired mode (e.g., 0644) by XOR it with + # 0777 (all 1s). We are interested in the bits that are currently 0 since + # it indicates that the actual mode is more permissive than the desired mode. + # Conversely, we dont care about the bits that are currently 1 because they + # cannot be any more permissive and we can safely ignore them. + # + # 2. Calculate the above result of ANDing the actual mode and the inverse + # mode. This will determine if any of the bits that would indicate a more + # permissive mode are set in the actual mode. + # + # 3. If the result is 0000. If the result is 0000, the files mode is equal + # to or less permissive than the desired mode (PASS). Otherwise, the files + # mode is more permissive than the desired mode (FAIL). + + max_mode = max_mode.rjust(4, '0') + binary_desired_mode = format('%04b', max_mode).to_i(2) + desired_mode_inverse = (binary_desired_mode ^ 0b111111111) + (desired_mode_inverse & file.mode).zero? ? false : true + end + def to_s "File #{source_path}" end @@ -212,6 +241,10 @@ module Inspec::Resources raise '`check_file_permission_by_mask` is not supported on Windows' end + def more_permissive_than?(*) + raise Inspec::Exceptions::ResourceSkipped, 'The `more_permissive_than?` matcher is not supported on your OS yet.' + end + def check_file_permission_by_user(access_type, user, path) access_rule = translate_perm_names(access_type) access_rule = convert_to_powershell_array(access_rule) diff --git a/test/unit/resources/file_test.rb b/test/unit/resources/file_test.rb index 5b9da4a46..d3b50c42e 100644 --- a/test/unit/resources/file_test.rb +++ b/test/unit/resources/file_test.rb @@ -22,6 +22,7 @@ describe Inspec::Resources::FileResource do resource.stubs(:file_permission_granted?).with('write', 'by_usergroup', 'by_specific_user').returns('test_result') resource.stubs(:file_permission_granted?).with('execute', 'by_usergroup', 'by_specific_user').returns('test_result') _(resource.content).must_equal 'content' + _(resource.more_permissive_than?('000').must_equal false) _(resource.exist?).must_equal true _(resource.mounted?).must_equal true _(resource.to_s).must_equal 'File /fakepath/fakefile' @@ -34,6 +35,8 @@ describe Inspec::Resources::FileResource do _(resource.suid).must_equal true _(resource.sgid).must_equal true _(resource.sticky).must_equal true + proc { resource.send(:more_permissive_than?, nil) }.must_raise(ArgumentError) + proc { resource.send(:more_permissive_than?, 0700) }.must_raise(ArgumentError) end it 'responds on Windows' do resource = MockLoader.new(:windows).load_resource('file', 'C:/fakepath/fakefile') @@ -79,3 +82,14 @@ describe Inspec::Resources::FileResource do proc { resource.send(:contain, nil) }.must_raise(RuntimeError) end end + +describe Inspec::Resources::FileResource do + let(:file) { stub(unix_mode_mask: 000, mode: 644) } + it 'responds on Ubuntu' do + resource = MockLoader.new(:ubuntu1404).load_resource('file', '/fakepath/fakefile') + _(resource.more_permissive_than?('755').must_equal false) + _(resource.more_permissive_than?('644').must_equal false) + _(resource.more_permissive_than?('640').must_equal true) + proc { resource.send(:more_permissive_than?, '0888') }.must_raise(ArgumentError) + end +end