Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow crontab resource to read crontab at user specified paths. #2328

Merged
merged 29 commits into from
Dec 7, 2017

Conversation

miah
Copy link
Contributor

@miah miah commented Nov 22, 2017

This PR adds support for reading crontab files at user specified paths as requested in #1461.

Example usage:

describe crontab('/etc/cron.d/some_crontab') do
  its('commands') { should include '/path/to/some/script' }
end

@miah miah requested a review from a team as a code owner November 22, 2017 00:52
Copy link
Contributor

@adamleff adamleff left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @miah! I love that we're enhancing this resource. However, I think some incorrect assumptions were made regarding the format of non-user crontab files and how they can be read in. Perhaps I tested on a different OS than you did, so I'm happy to be wrong about this as well.

@destination.nil? ? 'crontab for current user' : path_or_user
end

def path_or_user
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably make this a private method.

@@ -73,7 +77,7 @@ def parse_crontab_line(l)
end

def crontab_cmd
@user.nil? ? 'crontab -l' : "crontab -l -u #{@user}"
@destination.nil? ? 'crontab -l' : "crontab -l -u #{@destination}"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this works... -u specifically takes a user. On a stock Ubuntu 16.04 box, which has a /etc/cron.d/mdadm file, crontab -l -u /etc/cron.d/mdadm fails. I think you just need to read in the file.

@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 = path? ? inspec.file(@destination).content : inspec.command(crontab_cmd).stdout
ct.lines.map { |l| parse_crontab_line(l) }.compact
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-user crontabs, such as those in /etc/cron.d follow a different format. Specifically, before the command, there is another field for the user via which the command should be run. We'd need to adapt the resource to handle that new format for non-user crontabs.

@@ -0,0 +1,10 @@
#
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to provide a better mock file that has users in it. Here's the contents of the /etc/cron.d/mdadm file from the Ubuntu vagrant box I just spun up:

#
# cron.d/mdadm -- schedules periodic redundancy checks of MD devices
#
# Copyright © martin f. krafft <[email protected]>
# distributed under the terms of the Artistic Licence 2.0
#

# By default, run at 00:57 on every Sunday, but do nothing unless the day of
# the month is less than or equal to 7. Thus, only run on the first Sunday of
# each month. crontab(5) sucks, unfortunately, in this regard; therefore this
# hack (see #380425).
57 0 * * 0 root if [ -x /usr/share/mdadm/checkarray ] && [ $(date +\%d) -le 7 ]; then /usr/share/mdadm/checkarray --cron --all --idle --quiet; fi

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, my precise64 doesn't have this; even after I installed mdadm. =)

"

attr_reader :params

include CommentParser

def initialize(user = nil)
@user = user
def initialize(destination = nil)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am a little bit unsure about the overloading of the destination. It can be a user name or a path. Wouldn't it be cleaner to use something like:
describe crontab({path: '/etc/cron.d/some_crontab'}) and describe crontab({user: 'chris'})

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes will do!

@miah
Copy link
Contributor Author

miah commented Dec 1, 2017

This PR updates the crontab resource to parse system and user crontab files. To parse a system crontab file pass a {path: '/some/path'} to the resource. The resource still accepts a user passed in as a string describe crontab('root'). You may now match against the user field.

describe crontab({user: 'root'}) do
  its('commands') { should include '/path/to/some/script' }
  its('user') { should be 'root' }
end

describe crontab({path: '/etc/cron.d/some_crontab'}) do
  its('commands') { should include '/path/to/some/script' }
end

Copy link
Contributor

@adamleff adamleff left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@miah nice work! A couple of clean-up items and I think this will be ready to go.


require 'utils/parser'
require 'utils/filter'

# rubocop:disable Metrics/ClassLength
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be consistent with the way we disable in other files, can you please move this to the class definition itself?

class Crontab < Inspec.resource(1) # rubocop:disable Metrics/ClassLength

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
describe crontab({user: 'root'}) do
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The { } braces aren't necessary, Ruby will happily see that you're passing in a hash if you use the 2.0-and-greater syntax like you're doing:

describe crontab(user: 'root') do

@@ -25,51 +25,35 @@ class Crontab < Inspec.resource(1)
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, unnecessary { } characters :)

def initialize(user = nil)
@user = user
def initialize(opts = {})
Hash[opts.map { |k, v| [k.to_sym, v] }] if opts.respond_to?(:fetch)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're doing a check to see if it's a Hash on each of these 3 lines, so can we consolidate it so it reads a bit more clearly?

if opts.respond_to?(:fetch)
  # user supplied a Hash containing options
  @user = opts[:user]
  @path = opts[:path]
else
  # user passed in a string, and the prior behavior only supported
  # user crontabs, so assume they're supplying a user.
  @user = opts
  @path = nil
end

@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 = @path.nil? ? inspec.command(crontab_cmd).stdout : inspec.file(@path).content
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do the @path.nil? check a few times. What do you think about adding a helper method or to?

def is_system_crontab?
  !@path.nil?
end

def is_user_crontab?
  !@user.nil?
end

We could use that here, in the to_s method, and in parse_crontab_line.

def initialize(opts = {})
Hash[opts.map { |k, v| [k.to_sym, v] }] if opts.respond_to?(:fetch)
@user = opts.respond_to?(:fetch) ? opts.fetch(:user, nil) : opts
@path = opts.fetch(:path, nil) if opts.respond_to?(:fetch)
@params = read_crontab

return skip_resource 'The `crontab` resource is not supported on your OS.' unless inspec.os.unix?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While you're in here, can you change this to raise an exception?

raise Inspec::Exceptions::ResourceSkipped, 'The `crontab` resource is not supported on your OS.' unless inspec.os.unix?

And we should also add an exception if the user didn't supply us enough information (i.e. they gave us a hash that contains neither user or path):

raise Inspec::Exceptions::ResourceFailed, "A user or path must be supplied" if @user.nil? && @path.nil?

@miah
Copy link
Contributor Author

miah commented Dec 5, 2017

Hi,

I have addressed that list of clean up items. Please let me know if there is anything else I should consider. =)

Copy link
Contributor

@adamleff adamleff left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright, @miah! This looks great. Conditionally approved pending green tests after a rebase once we square away the unrelated Train issue.

miah added 20 commits December 6, 2017 12:19
…hen either calling the crontab command or reading the file directly, so break that out into a path? method.
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 <[email protected]>
file.

path? was removed as we're not overloading a @destination variable
anymore.

Signed-off-by: Miah Johnson <[email protected]>
miah added 8 commits December 6, 2017 12:19
We have three possible cases, current user, other user, or file path.
This accounts for all of them.

Signed-off-by: Miah Johnson <[email protected]>
Signed-off-by: Miah Johnson <[email protected]>
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 <[email protected]>
@miah miah force-pushed the miah/1461 branch 2 times, most recently from 2239aa9 to 858fd30 Compare December 6, 2017 20:33
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 <[email protected]>
@adamleff adamleff added the Type: Enhancement Improves an existing feature label Dec 7, 2017
Copy link
Contributor

@arlimus arlimus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What a wonderful improvement and update, thank you so much @miah !! 😄

Kudos Adam for your guidance as well :)

@@ -25,51 +24,40 @@ class Crontab < Inspec.resource(1)
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

love the new options specification!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants