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:
Miah Johnson 2017-12-07 04:50:07 -08:00 committed by Dominik Richter
parent 72af4a96f1
commit e33f4959e1
4 changed files with 174 additions and 34 deletions

View file

@ -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

View file

@ -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

View 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

View file

@ -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