mirror of
https://github.com/inspec/inspec
synced 2025-02-17 06:28:40 +00:00
Allow crontab resource to read crontab at user specified paths. (#2328)
* add a emulated /etc/cron.d/crondotd file to the mocking system. * test that we handle incoming paths correctly by rendering to_s. * We take in both users and a path, so lets call that destination. * To make the test pass we'll determine if we are dealing with a path or a user and return the correct string. * we will need the ability to determine if we are dealing with a path when either calling the crontab command or reading the file directly, so break that out into a path? method. * remove author field. * test contents of our crondotd file. * we have to explicitly make @destination a String to use include?. * when we get a path we use inspec.file to get conents, otherwise we run the crontab command. Signed-off-by: Miah Johnson <miah@chia-pet.org> * Add documentation for example usage with file path. Signed-off-by: Miah Johnson <miah@chia-pet.org> * Make path? and path_or_user private methods Signed-off-by: Miah Johnson <miah@chia-pet.org> * Add missing username filed to crondotd mock file Signed-off-by: Miah Johnson <miah@chia-pet.org> * Pass argument as a hash when testing file paths Signed-off-by: Miah Johnson <miah@chia-pet.org> * Expected results should include usernames when testing file paths Signed-off-by: Miah Johnson <miah@chia-pet.org> * Add special string `@yearly` test to crondotd mock file Signed-off-by: Miah Johnson <miah@chia-pet.org> * Add user to existing cron tests Signed-off-by: Miah Johnson <miah@chia-pet.org> * Rubocop says I need spaces after/before curly brackets Signed-off-by: Miah Johnson <miah@chia-pet.org> * Add user to crondotd file tests and add @yearly test Signed-off-by: Miah Johnson <miah@chia-pet.org> * Modify initialize to take options hash and be backwards compatible. Change initialize default argument to create a hash by default, though it is still possible to pass in a 'user' string argument. @user gets set with the argument value unless its a hash, in which case it tries to set the value of the user key, otherwise it becomes nil. @file gets set with the value of the path key, unless it doesn't exist in which case it becomes nil. All hash keys are symbolized to ensure consistent access. Signed-off-by: Miah Johnson <miah@chia-pet.org> * Check if @path is nil to determine if we run crontab command or parse file. path? was removed as we're not overloading a @destination variable anymore. Signed-off-by: Miah Johnson <miah@chia-pet.org> * if @user is nil assume current user otherwise crontab for @user Signed-off-by: Miah Johnson <miah@chia-pet.org> * Change to complete if rather than ternary. We have three possible cases, current user, other user, or file path. This accounts for all of them. Signed-off-by: Miah Johnson <miah@chia-pet.org> * Add user to the crontab FilterTable Signed-off-by: Miah Johnson <miah@chia-pet.org> * Remove path? and path_or_user Signed-off-by: Miah Johnson <miah@chia-pet.org> * Move crontab parsing to two methods, parse_user_crontab and parse_system_crontab Because a command in a crontab file could have spaces we must parse user and system crontabs differently. When we parse user crontabs the user field will either be nil, or the requested user. Both user and path parsers handle special strings (@yearly, @weekly, etc). And also account for position of user in these files (or adds it in user case) Signed-off-by: Miah Johnson <miah@chia-pet.org> * Update examples with user: and path: Signed-off-by: Miah Johnson <miah@chia-pet.org> * Add spaces after : in example docs Signed-off-by: Miah Johnson <miah@chia-pet.org> * Disable rubocop ClassLength check Signed-off-by: Miah Johnson <miah@chia-pet.org> * Moved rubocop ClassLength metric next to class instead of above the module. Remove unnecessary braces. Add is_system_crontab? and is_user_crontab helper methods and use them. Add tests to see if error conditions are raised when the resource is invoked with missing parameters (user, or path), and on a unsupported os. Change initialize to group all hash functions together and raise errors when user and path is unset. Also raise errors on unsupported operating systems. Change order of ternary and use is_system_crontab? rather than @path.nil? Signed-off-by: Miah Johnson <miah@chia-pet.org>
This commit is contained in:
parent
72af4a96f1
commit
e33f4959e1
4 changed files with 174 additions and 34 deletions
|
@ -1,15 +1,14 @@
|
|||
# encoding: utf-8
|
||||
# author: Adam Leff
|
||||
|
||||
require 'utils/parser'
|
||||
require 'utils/filter'
|
||||
|
||||
module Inspec::Resources
|
||||
class Crontab < Inspec.resource(1)
|
||||
class Crontab < Inspec.resource(1) # rubocop:disable Metrics/ClassLength
|
||||
name 'crontab'
|
||||
desc 'Use the crontab InSpec audit resource to test the contents of the crontab for a given user which contains information about scheduled tasks owned by that user.'
|
||||
example "
|
||||
describe crontab('root') do
|
||||
describe crontab(user: 'root') do
|
||||
its('commands') { should include '/path/to/some/script' }
|
||||
end
|
||||
|
||||
|
@ -25,51 +24,40 @@ module Inspec::Resources
|
|||
describe crontab.where { command =~ /a partial command string/ } do
|
||||
its('entries.length') { should cmp 1 }
|
||||
end
|
||||
|
||||
describe crontab(path: '/etc/cron.d/some_crontab') do
|
||||
its('commands') { should include '/path/to/some/script' }
|
||||
end
|
||||
"
|
||||
|
||||
attr_reader :params
|
||||
|
||||
include CommentParser
|
||||
|
||||
def initialize(user = nil)
|
||||
@user = user
|
||||
def initialize(opts = nil)
|
||||
if opts.respond_to?(:fetch)
|
||||
Hash[opts.map { |k, v| [k.to_sym, v] }]
|
||||
@user = opts.fetch(:user, nil)
|
||||
@path = opts.fetch(:path, nil)
|
||||
raise Inspec::Exceptions::ResourceFailed, 'A user or path must be supplied.' if @user.nil? && @path.nil?
|
||||
else
|
||||
@user = opts
|
||||
@path = nil
|
||||
end
|
||||
raise Inspec::Exceptions::ResourceSkipped, 'The `crontab` resource is not supported on your OS.' unless inspec.os.unix?
|
||||
@params = read_crontab
|
||||
|
||||
return skip_resource 'The `crontab` resource is not supported on your OS.' unless inspec.os.unix?
|
||||
end
|
||||
|
||||
def read_crontab
|
||||
inspec.command(crontab_cmd).stdout.lines.map { |l| parse_crontab_line(l) }.compact
|
||||
ct = is_system_crontab? ? inspec.file(@path).content : inspec.command(crontab_cmd).stdout
|
||||
ct.lines.map { |l| parse_crontab_line(l) }.compact
|
||||
end
|
||||
|
||||
def parse_crontab_line(l)
|
||||
data, = parse_comment_line(l, comment_char: '#', standalone_comments: false)
|
||||
return nil if data.nil? || data.empty?
|
||||
|
||||
case data
|
||||
when /@hourly .*/
|
||||
{ 'minute' => '0', 'hour' => '*', 'day' => '*', 'month' => '*', 'weekday' => '*', 'command' => data.split(/\s+/, 2).at(1) }
|
||||
when /@(midnight|daily) .*/
|
||||
{ 'minute' => '0', 'hour' => '0', 'day' => '*', 'month' => '*', 'weekday' => '*', 'command' => data.split(/\s+/, 2).at(1) }
|
||||
when /@weekly .*/
|
||||
{ 'minute' => '0', 'hour' => '0', 'day' => '*', 'month' => '*', 'weekday' => '0', 'command' => data.split(/\s+/, 2).at(1) }
|
||||
when /@monthly ./
|
||||
{ 'minute' => '0', 'hour' => '0', 'day' => '1', 'month' => '*', 'weekday' => '*', 'command' => data.split(/\s+/, 2).at(1) }
|
||||
when /@(annually|yearly) .*/
|
||||
{ 'minute' => '0', 'hour' => '0', 'day' => '1', 'month' => '1', 'weekday' => '*', 'command' => data.split(/\s+/, 2).at(1) }
|
||||
when /@reboot .*/
|
||||
{ 'minute' => '-1', 'hour' => '-1', 'day' => '-1', 'month' => '-1', 'weekday' => '-1', 'command' => data.split(/\s+/, 2).at(1) }
|
||||
else
|
||||
elements = data.split(/\s+/, 6)
|
||||
{
|
||||
'minute' => elements.at(0),
|
||||
'hour' => elements.at(1),
|
||||
'day' => elements.at(2),
|
||||
'month' => elements.at(3),
|
||||
'weekday' => elements.at(4),
|
||||
'command' => elements.at(5),
|
||||
}
|
||||
end
|
||||
is_system_crontab? ? parse_system_crontab(data) : parse_user_crontab(data)
|
||||
end
|
||||
|
||||
def crontab_cmd
|
||||
|
@ -84,19 +72,98 @@ module Inspec::Resources
|
|||
.add(:days, field: 'day')
|
||||
.add(:months, field: 'month')
|
||||
.add(:weekdays, field: 'weekday')
|
||||
.add(:user, field: 'user')
|
||||
.add(:commands, field: 'command')
|
||||
|
||||
# rebuild the crontab line from raw content
|
||||
filter.add(:content) { |t, _|
|
||||
t.entries.map do |e|
|
||||
[e.minute, e.hour, e.day, e.month, e.weekday, e.command].join(' ')
|
||||
[e.minute, e.hour, e.day, e.month, e.weekday, e.user, e.command].compact.join(' ')
|
||||
end.join("\n")
|
||||
}
|
||||
|
||||
filter.connect(self, :params)
|
||||
|
||||
def to_s
|
||||
@user.nil? ? 'crontab for current user' : "crontab for user #{@user}"
|
||||
if is_system_crontab?
|
||||
"crontab for path #{@path}"
|
||||
elsif is_user_crontab?
|
||||
"crontab for user #{@user}"
|
||||
else
|
||||
'crontab for current user'
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def is_system_crontab?
|
||||
!@path.nil?
|
||||
end
|
||||
|
||||
def is_user_crontab?
|
||||
!@user.nil?
|
||||
end
|
||||
|
||||
def parse_system_crontab(data)
|
||||
case data
|
||||
when /@hourly .*/
|
||||
elements = data.split(/\s+/, 3)
|
||||
{ 'minute' => '0', 'hour' => '*', 'day' => '*', 'month' => '*', 'weekday' => '*', 'user' => elements.at(1), 'command' => elements.at(2) }
|
||||
when /@(midnight|daily) .*/
|
||||
elements = data.split(/\s+/, 3)
|
||||
{ 'minute' => '0', 'hour' => '0', 'day' => '*', 'month' => '*', 'weekday' => '*', 'user' => elements.at(1), 'command' => elements.at(2) }
|
||||
when /@weekly .*/
|
||||
elements = data.split(/\s+/, 3)
|
||||
{ 'minute' => '0', 'hour' => '0', 'day' => '*', 'month' => '*', 'weekday' => '0', 'user' => elements.at(1), 'command' => elements.at(2) }
|
||||
when /@monthly ./
|
||||
elements = data.split(/\s+/, 3)
|
||||
{ 'minute' => '0', 'hour' => '0', 'day' => '1', 'month' => '*', 'weekday' => '*', 'user' => elements.at(1), 'command' => elements.at(2) }
|
||||
when /@(annually|yearly) .*/
|
||||
elements = data.split(/\s+/, 3)
|
||||
{ 'minute' => '0', 'hour' => '0', 'day' => '1', 'month' => '1', 'weekday' => '*', 'user' => elements.at(1), 'command' => elements.at(2) }
|
||||
when /@reboot .*/
|
||||
elements = data.split(/\s+/, 3)
|
||||
{ 'minute' => '-1', 'hour' => '-1', 'day' => '-1', 'month' => '-1', 'weekday' => '-1', 'user' => elements.at(1), 'command' => elements.at(2) }
|
||||
else
|
||||
elements = data.split(/\s+/, 7)
|
||||
{
|
||||
'minute' => elements.at(0),
|
||||
'hour' => elements.at(1),
|
||||
'day' => elements.at(2),
|
||||
'month' => elements.at(3),
|
||||
'weekday' => elements.at(4),
|
||||
'user' => elements.at(5),
|
||||
'command' => elements.at(6),
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def parse_user_crontab(data)
|
||||
case data
|
||||
when /@hourly .*/
|
||||
{ 'minute' => '0', 'hour' => '*', 'day' => '*', 'month' => '*', 'weekday' => '*', 'user' => @user, 'command' => data.split(/\s+/, 2).at(1) }
|
||||
when /@(midnight|daily) .*/
|
||||
{ 'minute' => '0', 'hour' => '0', 'day' => '*', 'month' => '*', 'weekday' => '*', 'user' => @user, 'command' => data.split(/\s+/, 2).at(1) }
|
||||
when /@weekly .*/
|
||||
{ 'minute' => '0', 'hour' => '0', 'day' => '*', 'month' => '*', 'weekday' => '0', 'user' => @user, 'command' => data.split(/\s+/, 2).at(1) }
|
||||
when /@monthly ./
|
||||
{ 'minute' => '0', 'hour' => '0', 'day' => '1', 'month' => '*', 'weekday' => '*', 'user' => @user, 'command' => data.split(/\s+/, 2).at(1) }
|
||||
when /@(annually|yearly) .*/
|
||||
{ 'minute' => '0', 'hour' => '0', 'day' => '1', 'month' => '1', 'weekday' => '*', 'user' => @user, 'command' => data.split(/\s+/, 2).at(1) }
|
||||
when /@reboot .*/
|
||||
{ 'minute' => '-1', 'hour' => '-1', 'day' => '-1', 'month' => '-1', 'weekday' => '-1', 'user' => @user, 'command' => data.split(/\s+/, 2).at(1) }
|
||||
else
|
||||
elements = data.split(/\s+/, 6)
|
||||
{
|
||||
'minute' => elements.at(0),
|
||||
'hour' => elements.at(1),
|
||||
'day' => elements.at(2),
|
||||
'month' => elements.at(3),
|
||||
'weekday' => elements.at(4),
|
||||
'user' => @user,
|
||||
'command' => elements.at(5),
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -182,6 +182,7 @@ class MockLoader
|
|||
'/etc/hosts.deny' => mockfile.call('hosts.deny'),
|
||||
'/fakepath/fakefile' => emptyfile.call,
|
||||
'C:/fakepath/fakefile' => emptyfile.call,
|
||||
'/etc/cron.d/crondotd' => mockfile.call('crondotd'),
|
||||
}
|
||||
|
||||
# create all mock commands
|
||||
|
|
13
test/unit/mock/files/crondotd
Normal file
13
test/unit/mock/files/crondotd
Normal file
|
@ -0,0 +1,13 @@
|
|||
#
|
||||
# This is a sample cron.d file for unit testing crontab with paths.
|
||||
#
|
||||
|
||||
|
||||
# entry number 1
|
||||
0 2 11 9 4 root /path/to/crondotd1
|
||||
|
||||
# entry number 2
|
||||
1 3 12 10 5 daemon /path/to/crondotd2 arg1 arg2
|
||||
|
||||
# entry number 3
|
||||
@yearly root /usr/local/bin/foo.sh bar
|
|
@ -39,6 +39,7 @@ describe 'Inspec::Resources::Crontab' do
|
|||
'day' => '11',
|
||||
'month' => '9',
|
||||
'weekday' => '4',
|
||||
'user' => nil,
|
||||
'command' => '/path/to/script1',
|
||||
},
|
||||
{
|
||||
|
@ -47,6 +48,7 @@ describe 'Inspec::Resources::Crontab' do
|
|||
'day' => '12',
|
||||
'month' => '10',
|
||||
'weekday' => '5',
|
||||
'user' => nil,
|
||||
'command' => '/path/to/script2 arg1 arg2'
|
||||
},
|
||||
])
|
||||
|
@ -76,6 +78,46 @@ describe 'Inspec::Resources::Crontab' do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'query by path' do
|
||||
let(:crontab) { load_resource('crontab', { path: '/etc/cron.d/crondotd' }) }
|
||||
|
||||
it 'prints a nice to_s string' do
|
||||
_(crontab.to_s).must_equal 'crontab for path /etc/cron.d/crondotd'
|
||||
end
|
||||
|
||||
it 'returns all params of the file' do
|
||||
_(crontab.params).must_equal(
|
||||
[{
|
||||
'minute' => '0',
|
||||
'hour' => '2',
|
||||
'day' => '11',
|
||||
'month' => '9',
|
||||
'weekday' => '4',
|
||||
'user' => 'root',
|
||||
'command' => '/path/to/crondotd1',
|
||||
},
|
||||
{
|
||||
'minute' => '1',
|
||||
'hour' => '3',
|
||||
'day' => '12',
|
||||
'month' => '10',
|
||||
'weekday' => '5',
|
||||
'user' => 'daemon',
|
||||
'command' => '/path/to/crondotd2 arg1 arg2',
|
||||
},
|
||||
{
|
||||
'minute' => '0',
|
||||
'hour' => '0',
|
||||
'day' => '1',
|
||||
'month' => '1',
|
||||
'weekday' => '*',
|
||||
'user' => 'root',
|
||||
'command' => '/usr/local/bin/foo.sh bar',
|
||||
}],
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'special strings' do
|
||||
let(:crontab) { load_resource('crontab', 'special') }
|
||||
|
||||
|
@ -87,6 +129,7 @@ describe 'Inspec::Resources::Crontab' do
|
|||
'day' => '*',
|
||||
'month' => '*',
|
||||
'weekday' => '*',
|
||||
'user' => 'special',
|
||||
'command' => '/bin/custom_script.sh',
|
||||
},
|
||||
{
|
||||
|
@ -95,6 +138,7 @@ describe 'Inspec::Resources::Crontab' do
|
|||
'day' => '1',
|
||||
'month' => '1',
|
||||
'weekday' => '*',
|
||||
'user' => 'special',
|
||||
'command' => '/usr/local/bin/foo.sh bar'
|
||||
},
|
||||
{
|
||||
|
@ -103,9 +147,24 @@ describe 'Inspec::Resources::Crontab' do
|
|||
'day' => '-1',
|
||||
'month' => '-1',
|
||||
'weekday' => '-1',
|
||||
'user' => 'special',
|
||||
'command' => '/bin/echo "Rebooting" > /var/log/rebooting.log'
|
||||
}
|
||||
])
|
||||
end
|
||||
end
|
||||
|
||||
describe 'it raises errors' do
|
||||
it 'raises error on unsupported os' do
|
||||
resource = MockLoader.new(:windows).load_resource('crontab', { user: 'special' })
|
||||
_(resource.resource_skipped?).must_equal true
|
||||
_(resource.resource_exception_message).must_equal 'The `crontab` resource is not supported on your OS.'
|
||||
end
|
||||
|
||||
it 'raises error when no user or path supplied' do
|
||||
resource = load_resource('crontab', {})
|
||||
_(resource.resource_failed?).must_equal true
|
||||
_(resource.resource_exception_message).must_equal 'A user or path must be supplied.'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Add table
Reference in a new issue