From 417b791baaf1b0555147d181bd52b967ede542cf Mon Sep 17 00:00:00 2001 From: Adam Leff Date: Thu, 9 Feb 2017 14:28:06 -0500 Subject: [PATCH] Adding new crontab resource The crontab resource parses a particular user's crontab file into individual entries and allows the user to assert information about each entry as needed. Signed-off-by: Adam Leff --- docs/resources/crontab.md.erb | 62 +++++++++++++++++++++ lib/inspec/resource.rb | 1 + lib/resources/crontab.rb | 83 +++++++++++++++++++++++++++++ test/helper.rb | 6 ++- test/unit/mock/cmd/crontab-foouser | 5 ++ test/unit/mock/cmd/crontab-root | 10 ++++ test/unit/resources/crontab_test.rb | 78 +++++++++++++++++++++++++++ 7 files changed, 244 insertions(+), 1 deletion(-) create mode 100644 docs/resources/crontab.md.erb create mode 100644 lib/resources/crontab.rb create mode 100644 test/unit/mock/cmd/crontab-foouser create mode 100644 test/unit/mock/cmd/crontab-root create mode 100644 test/unit/resources/crontab_test.rb diff --git a/docs/resources/crontab.md.erb b/docs/resources/crontab.md.erb new file mode 100644 index 000000000..c94732c59 --- /dev/null +++ b/docs/resources/crontab.md.erb @@ -0,0 +1,62 @@ +--- +title: About the crontab Resource +--- + +# crontab + +Use the `crontab` InSpec audit resource to test the crontab entries for a particular user on the system. + +## Syntax + +A `crontab` resource block declares a user (which defaults to the current user, if not specified), and then the details to be tested, such as the schedule elements for each crontab entry or the commands itself: + + describe crontab do + its('commands') { should include '/some/scheduled/task.sh' } + end + +## Matchers + +This InSpec audit resource has the following matchers: + +### be + +<%= partial "/shared/matcher_be" %> + +### cmp + +<%= partial "/shared/matcher_cmp" %> + +### eq + +<%= partial "/shared/matcher_eq" %> + +### include + +<%= partial "/shared/matcher_include" %> + +### match + +<%= partial "/shared/matcher_match" %> + +## Examples + +The following examples show how to use this InSpec audit resource. + +### Test that root's crontab has a particular command + + describe crontab('root') do + its('commands') { should include '/path/to/some/script' } + end + +### Test that myuser's crontab entry for command '/home/myuser/build.sh' runs every minute + + describe crontab('myuser').commands('/home/myuser/build.sh') do + its('hours') { should cmp '*' } + its('minutes') { should cmp '*' } + end + +### Test that the logged-in user's crontab has no tasks set to run on every hour and every minute + + describe crontab.where({'hour' => '*', 'minute' => '*'}) do + its('entries.length') { should cmp '0' } + end diff --git a/lib/inspec/resource.rb b/lib/inspec/resource.rb index a7926a26e..af9ccb237 100644 --- a/lib/inspec/resource.rb +++ b/lib/inspec/resource.rb @@ -79,6 +79,7 @@ require 'resources/bash' require 'resources/bond' require 'resources/bridge' require 'resources/command' +require 'resources/crontab' require 'resources/directory' require 'resources/etc_group' require 'resources/file' diff --git a/lib/resources/crontab.rb b/lib/resources/crontab.rb new file mode 100644 index 000000000..ccb1d86b3 --- /dev/null +++ b/lib/resources/crontab.rb @@ -0,0 +1,83 @@ +# encoding: utf-8 +# author: Adam Leff + +require 'utils/parser' +require 'utils/filter' + +module Inspec::Resources + class Crontab < Inspec.resource(1) + 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 + its('commands') { should include '/path/to/some/script' } + end + + describe crontab('myuser').commands('/home/myuser/build.sh') do + its('hours') { should cmp '*' } + its('minutes') { should cmp '*' } + end + + describe crontab.where({'hour' => '*', 'minute' => '*'}) do + its('entries.length') { should cmp '0' } + end + " + + attr_reader :params + + include CommentParser + + def initialize(user = nil) + @user = user + @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 + end + + def parse_crontab_line(l) + data, = parse_comment_line(l, comment_char: '#', standalone_comments: false) + return nil if data.nil? || data.empty? + + 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 + + def crontab_cmd + @user.nil? ? 'crontab -l' : "crontab -l -u #{@user}" + end + + filter = FilterTable.create + filter.add_accessor(:where) + .add_accessor(:entries) + .add(:minutes, field: 'minute') + .add(:hours, field: 'hour') + .add(:days, field: 'day') + .add(:months, field: 'month') + .add(:weekdays, field: 'weekday') + .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(' ') + end.join("\n") + } + + filter.connect(self, :params) + + def to_s + @user.nil? ? 'crontab for current user' : "crontab for user #{@user}" + end + end +end diff --git a/test/helper.rb b/test/helper.rb index 38d09cb7c..deb083bc0 100644 --- a/test/helper.rb +++ b/test/helper.rb @@ -274,7 +274,11 @@ class MockLoader "schtasks /query /v /fo csv /tn 'does-not-exist' | ConvertFrom-Csv | Select @{N='URI';E={$_.TaskName}},@{N='State';E={$_.Status.ToString()}},'Logon Mode','Last Result','Task To Run','Run As User','Scheduled Task State' | ConvertTo-Json -Compress" => cmd.call('schtasks-error'), # windows_task exist "schtasks /query /v /fo csv /tn 'WeLovePizza' | ConvertFrom-Csv | Select @{N='URI';E={$_.TaskName}},@{N='State';E={$_.Status.ToString()}},'Logon Mode','Last Result','Task To Run','Run As User','Scheduled Task State' | ConvertTo-Json -Compress" => cmd.call('schtasks-success'), - 'modinfo -F version dhcp' => cmd.call('modinfo-f-version-dhcp') + 'modinfo -F version dhcp' => cmd.call('modinfo-f-version-dhcp'), + # crontab display for root / current user + 'crontab -l' => cmd.call('crontab-root'), + # crontab display for non-current user + 'crontab -l -u foouser' => cmd.call('crontab-foouser') } @backend diff --git a/test/unit/mock/cmd/crontab-foouser b/test/unit/mock/cmd/crontab-foouser new file mode 100644 index 000000000..b117f6137 --- /dev/null +++ b/test/unit/mock/cmd/crontab-foouser @@ -0,0 +1,5 @@ +# +# This is a sample crontab file for unit testing, for user 'foouser' +# + +* * * * * /path/to/script3 diff --git a/test/unit/mock/cmd/crontab-root b/test/unit/mock/cmd/crontab-root new file mode 100644 index 000000000..a1e243d71 --- /dev/null +++ b/test/unit/mock/cmd/crontab-root @@ -0,0 +1,10 @@ +# +# This is a sample crontab file for unit testing +# + + +# entry number 1 +0 2 11 9 4 /path/to/script1 + +# entry number 2 +1 3 12 10 5 /path/to/script2 arg1 arg2 diff --git a/test/unit/resources/crontab_test.rb b/test/unit/resources/crontab_test.rb new file mode 100644 index 000000000..872b8bd87 --- /dev/null +++ b/test/unit/resources/crontab_test.rb @@ -0,0 +1,78 @@ +# encoding: utf-8 +# author: Adam Leff + +require 'helper' +require 'inspec/resource' + +describe 'Inspec::Resources::Crontab' do + let(:crontab) { load_resource('crontab') } + + it 'retrieve minutes via field' do + _(crontab.minutes).must_equal [ '0', '1' ] + end + + it 'retrieve hours via field' do + _(crontab.hours).must_equal [ '2', '3' ] + end + + it 'retrieve days via field' do + _(crontab.days).must_equal [ '11', '12' ] + end + + it 'retrieve months via field' do + _(crontab.months).must_equal [ '9', '10' ] + end + + it 'retrieve weekdays via field' do + _(crontab.weekdays).must_equal [ '4', '5' ] + end + + it 'retrieve commands via field' do + _(crontab.commands).must_equal [ '/path/to/script1', '/path/to/script2 arg1 arg2' ] + end + + it 'returns all params of the file' do + _(crontab.params).must_equal([ + { + 'minute' => '0', + 'hour' => '2', + 'day' => '11', + 'month' => '9', + 'weekday' => '4', + 'command' => '/path/to/script1', + }, + { + 'minute' => '1', + 'hour' => '3', + 'day' => '12', + 'month' => '10', + 'weekday' => '5', + 'command' => '/path/to/script2 arg1 arg2' + }, + ]) + end + + it 'prints a nice to_s string' do + _(crontab.to_s).must_equal "crontab for current user" + end + + describe 'filter by command' do + let(:entry) { crontab.commands(/script2/) } + + it 'returns the correct content' do + _(entry.content).must_equal '1 3 12 10 5 /path/to/script2 arg1 arg2' + end + + it 'prints a nice to_s string' do + _(entry.to_s).must_equal 'crontab for current user with command == /script2/' + end + end + + describe 'query by user' do + let(:crontab) { load_resource('crontab', 'foouser') } + + it 'prints a user-specific to_s string' do + _(crontab.to_s).must_equal 'crontab for user foouser' + end + end +end