inspec 0.12.0 → 0.14.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +39 -2
  3. data/bin/inspec +11 -9
  4. data/docs/matchers.rst +129 -0
  5. data/docs/resources.rst +64 -37
  6. data/inspec.gemspec +1 -1
  7. data/lib/bundles/inspec-compliance/cli.rb +1 -1
  8. data/lib/bundles/inspec-compliance/configuration.rb +1 -0
  9. data/lib/bundles/inspec-compliance/target.rb +16 -32
  10. data/lib/bundles/inspec-init/cli.rb +2 -0
  11. data/lib/bundles/inspec-supermarket.rb +13 -0
  12. data/lib/bundles/inspec-supermarket/api.rb +2 -0
  13. data/lib/bundles/inspec-supermarket/cli.rb +2 -2
  14. data/lib/bundles/inspec-supermarket/target.rb +11 -15
  15. data/lib/fetchers/local.rb +31 -0
  16. data/lib/fetchers/tar.rb +48 -0
  17. data/lib/fetchers/url.rb +100 -0
  18. data/lib/fetchers/zip.rb +47 -0
  19. data/lib/inspec.rb +2 -3
  20. data/lib/inspec/fetcher.rb +22 -0
  21. data/lib/inspec/metadata.rb +4 -2
  22. data/lib/inspec/plugins.rb +2 -0
  23. data/lib/inspec/plugins/fetcher.rb +97 -0
  24. data/lib/inspec/plugins/source_reader.rb +36 -0
  25. data/lib/inspec/profile.rb +92 -81
  26. data/lib/inspec/resource.rb +1 -0
  27. data/lib/inspec/runner.rb +15 -35
  28. data/lib/inspec/source_reader.rb +32 -0
  29. data/lib/inspec/version.rb +1 -1
  30. data/lib/matchers/matchers.rb +5 -6
  31. data/lib/resources/file.rb +8 -2
  32. data/lib/resources/passwd.rb +71 -45
  33. data/lib/resources/service.rb +13 -9
  34. data/lib/resources/shadow.rb +135 -0
  35. data/lib/source_readers/flat.rb +38 -0
  36. data/lib/source_readers/inspec.rb +78 -0
  37. data/lib/utils/base_cli.rb +2 -2
  38. data/lib/utils/parser.rb +1 -1
  39. data/lib/utils/plugin_registry.rb +93 -0
  40. data/test/docker_test.rb +1 -1
  41. data/test/helper.rb +62 -2
  42. data/test/integration/cookbooks/os_prepare/recipes/service.rb +4 -2
  43. data/test/integration/test/integration/default/compare_matcher_spec.rb +11 -0
  44. data/test/integration/test/integration/default/service_spec.rb +16 -1
  45. data/test/unit/fetchers.rb +61 -0
  46. data/test/unit/fetchers/local_test.rb +67 -0
  47. data/test/unit/fetchers/tar_test.rb +36 -0
  48. data/test/unit/fetchers/url_test.rb +152 -0
  49. data/test/unit/fetchers/zip_test.rb +36 -0
  50. data/test/unit/mock/files/passwd +1 -1
  51. data/test/unit/mock/files/shadow +2 -0
  52. data/test/unit/mock/profiles/complete-profile/libraries/testlib.rb +1 -0
  53. data/test/unit/plugin_test.rb +0 -1
  54. data/test/unit/profile_test.rb +32 -53
  55. data/test/unit/resources/passwd_test.rb +69 -14
  56. data/test/unit/resources/shadow_test.rb +67 -0
  57. data/test/unit/source_reader_test.rb +17 -0
  58. data/test/unit/source_readers/flat_test.rb +61 -0
  59. data/test/unit/source_readers/inspec_test.rb +38 -0
  60. data/test/unit/utils/passwd_parser_test.rb +1 -1
  61. metadata +40 -21
  62. data/lib/inspec/targets.rb +0 -10
  63. data/lib/inspec/targets/archive.rb +0 -33
  64. data/lib/inspec/targets/core.rb +0 -56
  65. data/lib/inspec/targets/dir.rb +0 -144
  66. data/lib/inspec/targets/file.rb +0 -33
  67. data/lib/inspec/targets/folder.rb +0 -38
  68. data/lib/inspec/targets/tar.rb +0 -61
  69. data/lib/inspec/targets/url.rb +0 -78
  70. data/lib/inspec/targets/zip.rb +0 -55
  71. data/test/unit/targets.rb +0 -132
@@ -0,0 +1,32 @@
1
+ # encoding: utf-8
2
+ # author: Dominik Richter
3
+ # author: Christoph Hartmann
4
+
5
+ require 'inspec/plugins'
6
+ require 'utils/plugin_registry'
7
+
8
+ module Inspec
9
+ # Pre-checking of target resolution. Make sure that SourceReader plugins
10
+ # always receive a fetcher.
11
+ class SourceReaderRegistry < PluginRegistry
12
+ def resolve(target)
13
+ return nil if target.nil?
14
+ unless target.is_a? Inspec::Plugins::Fetcher
15
+ fail "SourceReader cannot resolve targets that aren't Fetchers: #{target.class}"
16
+ end
17
+ super(target)
18
+ end
19
+ end
20
+
21
+ SourceReader = SourceReaderRegistry.new
22
+
23
+ def self.source_reader(version)
24
+ if version != 1
25
+ fail 'Only source readers version 1 is supported!'
26
+ end
27
+ Inspec::Plugins::SourceReader
28
+ end
29
+ end
30
+
31
+ require 'source_readers/inspec'
32
+ require 'source_readers/flat'
@@ -3,5 +3,5 @@
3
3
  # author: Christoph Hartmann
4
4
 
5
5
  module Inspec
6
- VERSION = '0.12.0'.freeze
6
+ VERSION = '0.14.0'.freeze
7
7
  end
@@ -229,23 +229,22 @@ end
229
229
  RSpec::Matchers.define :cmp do |expected|
230
230
 
231
231
  def integer?(value)
232
- return true if value =~ /\A\d+\Z/
233
- false
232
+ !(value =~ /\A\d+\Z/).nil?
234
233
  end
235
234
 
236
235
  def float?(value)
237
- return true if Float(value)
238
- false
236
+ Float(value)
237
+ true
239
238
  rescue ArgumentError => _ex
240
239
  false
241
240
  end
242
241
 
243
242
  def octal?(value)
244
- return true if value =~ /\A0+\d+\Z/
245
- false
243
+ !(value =~ /\A0+\d+\Z/).nil?
246
244
  end
247
245
 
248
246
  match do |actual|
247
+ actual = actual[0] if actual.is_a?(Array) && !expected.is_a?(Array) && actual.length == 1
249
248
  # if actual and expected are strings
250
249
  if expected.is_a?(String) && actual.is_a?(String)
251
250
  actual.casecmp(expected) == 0
@@ -5,7 +5,7 @@
5
5
  # license: All rights reserved
6
6
 
7
7
  module Inspec::Resources
8
- class File < Inspec.resource(1)
8
+ class File < Inspec.resource(1) # rubocop:disable Metrics/ClassLength
9
9
  name 'file'
10
10
  desc 'Use the file InSpec audit resource to test all system file types, including files, directories, symbolic links, named pipes, sockets, character devices, block devices, and doors.'
11
11
  example "
@@ -29,7 +29,7 @@ module Inspec::Resources
29
29
  %w{
30
30
  type exist? file? block_device? character_device? socket? directory?
31
31
  symlink? pipe? mode mode? owner owned_by? group grouped_into? link_target
32
- link_path linked_to? content mtime size selinux_label immutable?
32
+ link_path linked_to? mtime size selinux_label immutable?
33
33
  product_version file_version version? md5sum sha256sum
34
34
  }.each do |m|
35
35
  define_method m.to_sym do |*args|
@@ -37,6 +37,12 @@ module Inspec::Resources
37
37
  end
38
38
  end
39
39
 
40
+ def content
41
+ res = file.content
42
+ return nil if res.nil?
43
+ res.force_encoding('utf-8')
44
+ end
45
+
40
46
  def contain(*_)
41
47
  fail 'Contain is not supported. Please use standard RSpec matchers.'
42
48
  end
@@ -13,88 +13,114 @@
13
13
  # - home directory
14
14
  # - command
15
15
 
16
- # usage:
17
- #
18
- # describe passwd do
19
- # its(:usernames) { should eq ['root'] }
20
- # its(:uids) { should eq [0] }
21
- # end
22
- #
23
- # describe passwd.uid(0) do
24
- # its(:username) { should eq 'root' }
25
- # its(:count) { should eq 1 }
26
- # end
27
-
28
16
  require 'utils/parser'
29
17
 
30
18
  class Passwd < Inspec.resource(1)
31
19
  name 'passwd'
32
20
  desc 'Use the passwd InSpec audit resource to test the contents of /etc/passwd, which contains the following information for users that may log into the system and/or as users that own running processes.'
33
21
  example "
34
- describe passwd.uid(0) do
35
- its('username') { should eq 'root' }
22
+ describe passwd do
23
+ its('users') { should_not include 'forbidden_user' }
24
+ end
25
+
26
+ describe passwd.uids(0) do
27
+ its('users') { should cmp 'root' }
36
28
  its('count') { should eq 1 }
37
29
  end
30
+
31
+ describe passwd.shells(/nologin/) do
32
+ # find all users with a nologin shell
33
+ its('users') { should_not include 'my_login_user' }
34
+ end
38
35
  "
39
36
 
40
37
  include PasswdParser
41
38
 
42
39
  attr_reader :uid
43
- attr_reader :parsed
40
+ attr_reader :params
41
+ attr_reader :content
42
+ attr_reader :lines
44
43
 
45
- def initialize(path = nil)
44
+ def initialize(path = nil, opts = nil)
45
+ opts ||= {}
46
46
  @path = path || '/etc/passwd'
47
- @content = inspec.file(@path).content
48
- @parsed = parse_passwd(@content)
47
+ @content = opts[:content] || inspec.file(@path).content
48
+ @lines = @content.to_s.split("\n")
49
+ @filters = opts[:filters] || ''
50
+ @params = parse_passwd(@content)
49
51
  end
50
52
 
51
- # call passwd().uid(0)
52
- # returns a seperate object with reference to this object
53
- def uid(uid)
54
- PasswdUid.new(self, uid)
53
+ def filter(hm = {})
54
+ return self if hm.nil? || hm.empty?
55
+ res = @params
56
+ filters = ''
57
+ hm.each do |attr, condition|
58
+ condition = condition.to_s if condition.is_a? Integer
59
+ filters += " #{attr} = #{condition.inspect}"
60
+ res = res.find_all do |line|
61
+ case line[attr.to_s]
62
+ when condition
63
+ true
64
+ else
65
+ false
66
+ end
67
+ end
68
+ end
69
+ content = res.map { |x| x.values.join(':') }.join("\n")
70
+ Passwd.new(@path, content: content, filters: @filters + filters)
55
71
  end
56
72
 
57
73
  def usernames
58
- map_data('name')
74
+ warn '[DEPRECATION] `passwd.usernames` is deprecated. Please use `passwd.users` instead. It will be removed in version 1.0.0.'
75
+ users
59
76
  end
60
77
 
61
- def passwords
62
- map_data('password')
78
+ def username
79
+ warn '[DEPRECATION] `passwd.user` is deprecated. Please use `passwd.users` instead. It will be removed in version 1.0.0.'
80
+ users[0]
63
81
  end
64
82
 
65
- def uids
66
- map_data('uid')
83
+ def uid(x)
84
+ warn '[DEPRECATION] `passwd.uid(arg)` is deprecated. Please use `passwd.uids(arg)` instead. It will be removed in version 1.0.0.'
85
+ uids(x)
67
86
  end
68
87
 
69
- def gids
70
- map_data('gid')
88
+ def users(name = nil)
89
+ name.nil? ? map_data('user') : filter(user: name)
71
90
  end
72
91
 
73
- def to_s
74
- '/etc/passwd'
92
+ def passwords(password = nil)
93
+ password.nil? ? map_data('password') : filter(password: password)
75
94
  end
76
95
 
77
- private
96
+ def uids(uid = nil)
97
+ uid.nil? ? map_data('uid') : filter(uid: uid)
98
+ end
78
99
 
79
- def map_data(id)
80
- @parsed.map {|x|
81
- x[id]
82
- }
100
+ def gids(gid = nil)
101
+ gid.nil? ? map_data('gid') : filter(gid: gid)
83
102
  end
84
- end
85
103
 
86
- # object that hold a specifc uid view on passwd
87
- class PasswdUid
88
- def initialize(passwd, uid)
89
- @passwd = passwd
90
- @users = @passwd.parsed.select { |x| x['uid'] == uid.to_s }
104
+ def homes(home = nil)
105
+ home.nil? ? map_data('home') : filter(home: home)
91
106
  end
92
107
 
93
- def username
94
- @users.at(0)['name']
108
+ def shells(shell = nil)
109
+ shell.nil? ? map_data('shell') : filter(shell: shell)
110
+ end
111
+
112
+ def to_s
113
+ f = @filters.empty? ? '' : ' with'+@filters
114
+ "/etc/passwd#{f}"
95
115
  end
96
116
 
97
117
  def count
98
- @users.size
118
+ @params.length
119
+ end
120
+
121
+ private
122
+
123
+ def map_data(id)
124
+ @params.map { |x| x[id] }
99
125
  end
100
126
  end
@@ -189,7 +189,7 @@ end
189
189
  # @see: http://www.freedesktop.org/software/systemd/man/systemd-system.conf.html
190
190
  class Systemd < ServiceManager
191
191
  def initialize(inspec, service_ctl = nil)
192
- @service_ctl ||= 'systemctl'
192
+ @service_ctl = service_ctl || 'systemctl'
193
193
  super
194
194
  end
195
195
 
@@ -270,7 +270,7 @@ end
270
270
  # @see: http://upstart.ubuntu.com
271
271
  class Upstart < ServiceManager
272
272
  def initialize(service_name, service_ctl = nil)
273
- @service_ctl ||= 'initctl'
273
+ @service_ctl = service_ctl || 'initctl'
274
274
  super
275
275
  end
276
276
 
@@ -311,6 +311,8 @@ class Upstart < ServiceManager
311
311
  config = inspec.file("/etc/init/#{service_name}.conf").content
312
312
  end
313
313
 
314
+ # disregard if the config does not exist
315
+ return nil if config.nil?
314
316
  enabled = !config[/^\s*start on/].nil?
315
317
 
316
318
  # implement fallback for Ubuntu 10.04
@@ -325,8 +327,10 @@ class Upstart < ServiceManager
325
327
  end
326
328
 
327
329
  def version
328
- @version ||= Gem::Version.new(inspec.command("#{service_ctl} --version")
329
- .stdout.match(/\(upstart ([^\)]+)\)/)[1])
330
+ @version ||= (
331
+ out = inspec.command("#{service_ctl} --version").stdout
332
+ Gem::Version.new(out[/\(upstart ([^\)]+)\)/, 1])
333
+ )
330
334
  end
331
335
  end
332
336
 
@@ -334,7 +338,7 @@ class SysV < ServiceManager
334
338
  RUNLEVELS = { 0=>false, 1=>false, 2=>false, 3=>false, 4=>false, 5=>false, 6=>false }.freeze
335
339
 
336
340
  def initialize(service_name, service_ctl = nil)
337
- @service_ctl ||= 'service'
341
+ @service_ctl = service_ctl || 'service'
338
342
  super
339
343
  end
340
344
 
@@ -386,7 +390,7 @@ end
386
390
  # @see: https://www.freebsd.org/cgi/man.cgi?query=rc.conf&sektion=5
387
391
  class BSDInit < ServiceManager
388
392
  def initialize(service_name, service_ctl = nil)
389
- @service_ctl ||= 'service'
393
+ @service_ctl = service_ctl || 'service'
390
394
  super
391
395
  end
392
396
 
@@ -423,7 +427,7 @@ end
423
427
 
424
428
  class Runit < ServiceManager
425
429
  def initialize(service_name, service_ctl = nil)
426
- @service_ctl ||= 'sv'
430
+ @service_ctl = service_ctl || 'sv'
427
431
  super
428
432
  end
429
433
 
@@ -452,7 +456,7 @@ end
452
456
  # new launctl on macos 10.10
453
457
  class LaunchCtl < ServiceManager
454
458
  def initialize(service_name, service_ctl = nil)
455
- @service_ctl ||= 'launchctl'
459
+ @service_ctl = service_ctl || 'launchctl'
456
460
  super
457
461
  end
458
462
 
@@ -562,7 +566,7 @@ end
562
566
  # Solaris services
563
567
  class Svcs < ServiceManager
564
568
  def initialize(service_name, service_ctl = nil)
565
- @service_ctl ||= 'svcs'
569
+ @service_ctl = service_ctl || 'svcs'
566
570
  super
567
571
  end
568
572
 
@@ -0,0 +1,135 @@
1
+ # encoding: utf-8
2
+ # copyright: 2016, Chef Software Inc.
3
+ # author: Dominik Richter
4
+ # author: Christoph Hartmann
5
+
6
+ require 'forwardable'
7
+
8
+ # The file format consists of
9
+ # - user
10
+ # - password
11
+ # - last_change
12
+ # - min_days before password change
13
+ # - max_days until password change
14
+ # - warn_days before warning about expiry
15
+ # - inactive_days before deactivating the account
16
+ # - expiry_date when this account will expire
17
+
18
+ class Shadow < Inspec.resource(1)
19
+ name 'shadow'
20
+ desc 'Use the shadow InSpec resource to test the contents of /etc/shadow, '\
21
+ 'which contains the following information for users that may log into '\
22
+ 'the system and/or as users that own running processes.'
23
+ example "
24
+ describe shadow do
25
+ its('users') { should_not include 'forbidden_user' }
26
+ end
27
+
28
+ describe shadow.users('bin') do
29
+ its('password') { should cmp 'x' }
30
+ its('count') { should eq 1 }
31
+ end
32
+ "
33
+
34
+ extend Forwardable
35
+ attr_reader :params
36
+ attr_reader :content
37
+ attr_reader :lines
38
+
39
+ def initialize(path = '/etc/shadow', opts = nil)
40
+ opts ||= {}
41
+ @path = path || '/etc/shadow'
42
+ @content = opts[:content] || inspec.file(@path).content
43
+ @lines = @content.to_s.split("\n")
44
+ @filters = opts[:filters] || ''
45
+ @params = @lines.map { |l| parse_shadow_line(l) }
46
+ end
47
+
48
+ def filter(hm = {})
49
+ return self if hm.nil? || hm.empty?
50
+ res = @params
51
+ filters = ''
52
+ hm.each do |attr, condition|
53
+ condition = condition.to_s if condition.is_a? Integer
54
+ filters += " #{attr} = #{condition.inspect}"
55
+ res = res.find_all do |line|
56
+ case line[attr.to_s]
57
+ when condition
58
+ true
59
+ else
60
+ false
61
+ end
62
+ end
63
+ end
64
+ content = res.map { |x| x.values.join(':') }.join("\n")
65
+ Shadow.new(@path, content: content, filters: @filters + filters)
66
+ end
67
+
68
+ def entries
69
+ @lines.map { |line| Shadow.new(@path, content: line, filters: @filters) }
70
+ end
71
+
72
+ def users(name = nil)
73
+ name.nil? ? map_data('user') : filter(user: name)
74
+ end
75
+
76
+ def passwords(password = nil)
77
+ password.nil? ? map_data('password') : filter(password: password)
78
+ end
79
+
80
+ def last_changes(filter_by = nil)
81
+ filter_by.nil? ? map_data('last_change') : filter(last_change: filter_by)
82
+ end
83
+
84
+ def min_days(filter_by = nil)
85
+ filter_by.nil? ? map_data('min_days') : filter(min_days: filter_by)
86
+ end
87
+
88
+ def max_days(filter_by = nil)
89
+ filter_by.nil? ? map_data('max_days') : filter(max_days: filter_by)
90
+ end
91
+
92
+ def warn_days(filter_by = nil)
93
+ filter_by.nil? ? map_data('warn_days') : filter(warn_days: filter_by)
94
+ end
95
+
96
+ def inactive_days(filter_by = nil)
97
+ filter_by.nil? ? map_data('inactive_days') : filter(inactive_days: filter_by)
98
+ end
99
+
100
+ def expiry_dates(filter_by = nil)
101
+ filter_by.nil? ? map_data('expiry_date') : filter(expiry_date: filter_by)
102
+ end
103
+
104
+ def to_s
105
+ f = @filters.empty? ? '' : ' with'+@filters
106
+ "/etc/shadow#{f}"
107
+ end
108
+
109
+ def_delegator :@params, :length, :count
110
+
111
+ private
112
+
113
+ def map_data(id)
114
+ @params.map { |x| x[id] }
115
+ end
116
+
117
+ # Parse a line of /etc/shadow
118
+ #
119
+ # @param [String] line a line of /etc/shadow
120
+ # @return [Hash] Map of entries in this line
121
+ def parse_shadow_line(line)
122
+ x = line.split(':')
123
+ {
124
+ 'user' => x.at(0),
125
+ 'password' => x.at(1),
126
+ 'last_change' => x.at(2),
127
+ 'min_days' => x.at(3),
128
+ 'max_days' => x.at(4),
129
+ 'warn_days' => x.at(5),
130
+ 'inactive_days' => x.at(6),
131
+ 'expiry_date' => x.at(7),
132
+ 'reserved' => x.at(8),
133
+ }
134
+ end
135
+ end