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
@@ -9,6 +9,8 @@ module Inspec
9
9
  module Plugins
10
10
  autoload :Resource, 'inspec/plugins/resource'
11
11
  autoload :CLI, 'inspec/plugins/cli'
12
+ autoload :Fetcher, 'inspec/plugins/fetcher'
13
+ autoload :SourceReader, 'inspec/plugins/source_reader'
12
14
  end
13
15
 
14
16
  # PLEASE NOTE: The Plugin system is an internal mechanism for connecting
@@ -0,0 +1,97 @@
1
+ # encoding: utf-8
2
+ # author: Dominik Richter
3
+ # author: Christoph Hartmann
4
+
5
+ require 'utils/plugin_registry'
6
+
7
+ module Inspec
8
+ module Plugins
9
+ class Fetcher < PluginRegistry::Plugin
10
+ def self.plugin_registry
11
+ Inspec::Fetcher
12
+ end
13
+
14
+ # Provide a list of files that are available to this fetcher.
15
+ #
16
+ # @return [Array[String]] A list of filenames
17
+ def files
18
+ fail "Fetcher #{self} does not implement `files()`. This is required."
19
+ end
20
+
21
+ # Read a file using this fetcher. The name must correspond to a file
22
+ # available to this fetcher. Use #files to retrieve the list of
23
+ # files.
24
+ #
25
+ # @param [String] _file The filename you are interested in
26
+ # @return [String] The file's contents
27
+ def read(_file)
28
+ fail "Fetcher #{self} does not implement `read(...)`. This is required."
29
+ end
30
+
31
+ def relative_target
32
+ RelFetcher.new(self)
33
+ end
34
+ end
35
+
36
+ class RelFetcher < Fetcher
37
+ attr_reader :files
38
+ attr_reader :prefix
39
+
40
+ def initialize(fetcher)
41
+ @parent = fetcher
42
+ @prefix = get_prefix(fetcher.files)
43
+ @files = fetcher.files.find_all { |x| x.start_with? prefix }
44
+ .map { |x| x[prefix.length..-1] }
45
+ end
46
+
47
+ def abs_path(file)
48
+ prefix + file
49
+ end
50
+
51
+ def read(file)
52
+ @parent.read(abs_path(file))
53
+ end
54
+
55
+ private
56
+
57
+ def get_prefix(fs)
58
+ return '' if fs.empty?
59
+ sorted = fs.sort_by(&:length)
60
+ get_folder_prefix(sorted)
61
+ end
62
+
63
+ def get_folder_prefix(fs, first_iteration = true)
64
+ return get_files_prefix(fs) if fs.length == 1
65
+ pre = fs[0] + File::SEPARATOR
66
+ rest = fs[1..-1]
67
+ if rest.all? { |i| i.start_with? pre }
68
+ return get_folder_prefix(rest, false)
69
+ end
70
+ return get_files_prefix(fs) if first_iteration
71
+ fs
72
+ end
73
+
74
+ def get_files_prefix(fs)
75
+ return '' if fs.empty?
76
+
77
+ file = fs[0]
78
+ bn = File.basename(file)
79
+ # no more prefixes
80
+ return '' if bn == file
81
+
82
+ i = file.rindex(bn)
83
+ pre = file[0..i-1]
84
+
85
+ rest = fs.find_all { |f| !f.start_with?(pre) }
86
+ return pre if rest.empty?
87
+
88
+ new_pre = get_prefix(rest)
89
+ return new_pre if pre.start_with? new_pre
90
+ # edge case: completely different prefixes; retry prefix detection
91
+ a = File.dirname(pre + 'a')
92
+ b = File.dirname(new_pre + 'b')
93
+ get_prefix([a, b])
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,36 @@
1
+ # encoding: utf-8
2
+ # author: Dominik Richter
3
+ # author: Christoph Hartmann
4
+
5
+ require 'utils/plugin_registry'
6
+
7
+ module Inspec
8
+ module Plugins
9
+ class SourceReader < PluginRegistry::Plugin
10
+ def self.plugin_registry
11
+ Inspec::SourceReader
12
+ end
13
+
14
+ # Retrieve this profile's metadata.
15
+ #
16
+ # @return [Inspec::Metadata] profile metadata
17
+ def metadata
18
+ fail "SourceReader #{self} does not implement `metadata()`. This method is required"
19
+ end
20
+
21
+ # Retrieve this profile's tests
22
+ #
23
+ # @return [Hash] Collection with references pointing to test contents
24
+ def tests
25
+ fail "SourceReader #{self} does not implement `tests()`. This method is required"
26
+ end
27
+
28
+ # Retrieve this profile's libraries
29
+ #
30
+ # @return [Hash] Collection with references pointing to library contents
31
+ def libraries
32
+ fail "SourceReader #{self} does not implement `libraries()`. This method is required"
33
+ end
34
+ end
35
+ end
36
+ end
@@ -3,71 +3,53 @@
3
3
  # author: Dominik Richter
4
4
  # author: Christoph Hartmann
5
5
 
6
+ require 'forwardable'
7
+ require 'inspec/fetcher'
8
+ require 'inspec/source_reader'
6
9
  require 'inspec/metadata'
7
- require 'pathname'
8
10
 
9
11
  module Inspec
10
12
  class Profile # rubocop:disable Metrics/ClassLength
11
- def self.from_path(path, options = nil)
12
- opt = {}
13
- options.each { |k, v| opt[k.to_sym] = v } unless options.nil?
14
- opt[:path] = path
15
- Profile.new(opt)
13
+ extend Forwardable
14
+ attr_reader :path
15
+
16
+ def self.for_target(target, opts)
17
+ # Fetchers retrieve file contents
18
+ opts[:target] = target
19
+ fetcher = Inspec::Fetcher.resolve(target)
20
+ return nil if fetcher.nil?
21
+ # Source readers understand the target's structure and provide
22
+ # access to tests, libraries, and metadata
23
+ reader = Inspec::SourceReader.resolve(fetcher.relative_target)
24
+ return nil if reader.nil?
25
+ new(reader, opts)
16
26
  end
17
27
 
18
- attr_reader :params
19
- attr_reader :path
20
- attr_reader :metadata
28
+ attr_reader :source_reader
29
+ def_delegator :@source_reader, :tests
30
+ def_delegator :@source_reader, :libraries
31
+ def_delegator :@source_reader, :metadata
21
32
 
22
33
  # rubocop:disable Metrics/AbcSize
23
- def initialize(options = nil)
34
+ def initialize(source_reader, options = nil)
24
35
  @options = options || {}
25
- @logger = options[:logger] || Logger.new(nil)
26
- @path = @options[:path]
27
- @profile_id = options[:id]
28
-
29
- @runner = Runner.new(
30
- id: @profile_id,
31
- backend: :mock,
32
- test_collector: @options.delete(:test_collector),
33
- )
34
-
35
- # we're checking a profile, we don't care if it runs on the host machine
36
- @options[:ignore_supports] = true
37
- tests, libs, metadata = @runner.add_tests([@path], @options)
38
- @content = tests + libs + metadata
39
-
40
- # NB if you want to check more than one profile, use one
41
- # Inspec::Profile#from_file per profile
42
- @metadata_source = metadata.first
43
- @metadata = Metadata.from_ref(@metadata_source[:ref], @metadata_source[:content], @profile_id, @logger)
44
- @params = @metadata.params
45
- @profile_id ||= params[:name]
46
- @params[:name] = @profile_id
47
- @params[:rules] = rules = {}
36
+ @target = @options.delete(:target)
37
+ @logger = @options[:logger] || Logger.new(nil)
38
+ @source_reader = source_reader
39
+ @profile_id = @options[:id]
40
+ Metadata.finalize(@source_reader.metadata, @profile_id)
41
+ end
48
42
 
49
- @runner.rules.each do |id, rule|
50
- file = rule.instance_variable_get(:@__file)
51
- rules[file] ||= {}
52
- rules[file][id] = {
53
- title: rule.title,
54
- desc: rule.desc,
55
- impact: rule.impact,
56
- checks: rule.instance_variable_get(:@checks),
57
- code: rule.instance_variable_get(:@__code),
58
- source_location: rule.instance_variable_get(:@__source_location),
59
- group_title: rule.instance_variable_get(:@__group_title),
60
- }
61
- end
43
+ def params
44
+ @params ||= load_params
62
45
  end
63
46
 
64
47
  def info
65
- res = @params.dup
48
+ res = params.dup
66
49
  rules = {}
67
50
  res[:rules].each do |gid, group|
68
51
  next if gid.to_s.empty?
69
- path = gid.sub(File.join(@path, ''), '')
70
- rules[path] = { title: path, rules: {} }
52
+ rules[gid] = { title: gid, rules: {} }
71
53
  group.each do |id, rule|
72
54
  next if id.to_s.empty?
73
55
  data = rule.dup
@@ -75,10 +57,10 @@ module Inspec
75
57
  data[:impact] ||= 0.5
76
58
  data[:impact] = 1.0 if data[:impact] > 1.0
77
59
  data[:impact] = 0.0 if data[:impact] < 0.0
78
- rules[path][:rules][id] = data
60
+ rules[gid][:rules][id] = data
79
61
  # TODO: temporarily flatten the group down; replace this with
80
62
  # proper hierarchy later on
81
- rules[path][:title] = data[:group_title]
63
+ rules[gid][:title] = data[:group_title]
82
64
  end
83
65
  end
84
66
  res[:rules] = rules
@@ -95,7 +77,7 @@ module Inspec
95
77
  summary: {
96
78
  valid: false,
97
79
  timestamp: Time.now.iso8601,
98
- location: @path,
80
+ location: @target,
99
81
  profile: nil,
100
82
  controls: 0,
101
83
  },
@@ -123,27 +105,27 @@ module Inspec
123
105
  result[:errors].push(entry.call(file, line, column, control, msg))
124
106
  }
125
107
 
126
- @logger.info "Checking profile in #{@path}"
127
-
128
- if @content.any? { |h| h[:type] == :metadata && h[:ref] =~ /metadata\.rb$/ }
129
- warn.call(Pathname.new(path).join('metadata.rb'), 0, 0, nil, 'The use of `metadata.rb` is deprecated. Use `inspec.yml`.')
108
+ @logger.info "Checking profile in #{@target}"
109
+ meta_path = @source_reader.target.abs_path(@source_reader.metadata.ref)
110
+ if meta_path =~ /metadata\.rb$/
111
+ warn.call(@target, 0, 0, nil, 'The use of `metadata.rb` is deprecated. Use `inspec.yml`.')
130
112
  end
131
113
 
132
114
  # verify metadata
133
- m_errors, m_warnings = @metadata.valid
134
- m_errors.each { |msg| error.call(@metadata_source[:ref], 0, 0, nil, msg) }
135
- m_warnings.each { |msg| warn.call(@metadata_source[:ref], 0, 0, nil, msg) }
136
- m_unsupported = @metadata.unsupported
137
- m_unsupported.each { |u| warn.call(@metadata_source[:ref], 0, 0, nil, "doesn't support: #{u}") }
115
+ m_errors, m_warnings = metadata.valid
116
+ m_errors.each { |msg| error.call(meta_path, 0, 0, nil, msg) }
117
+ m_warnings.each { |msg| warn.call(meta_path, 0, 0, nil, msg) }
118
+ m_unsupported = metadata.unsupported
119
+ m_unsupported.each { |u| warn.call(meta_path, 0, 0, nil, "doesn't support: #{u}") }
138
120
  @logger.info 'Metadata OK.' if m_errors.empty? && m_unsupported.empty?
139
121
 
140
122
  # extract profile name
141
- result[:summary][:profile] = @metadata.params[:name]
123
+ result[:summary][:profile] = metadata.params[:name]
142
124
 
143
125
  # check if the profile is using the old test directory instead of the
144
126
  # new controls directory
145
- if @content.any? { |h| h[:type] == :test && h[:ref] =~ %r{test/[^/]+$} }
146
- warn.call(Pathname.new(path).join('test'), 0, 0, nil, 'Profile uses deprecated `test` directory, rename it to `controls`.')
127
+ if @source_reader.tests.keys.any? { |x| x =~ %r{^test/$} }
128
+ warn.call(@target, 0, 0, nil, 'Profile uses deprecated `test` directory, rename it to `controls`.')
147
129
  end
148
130
 
149
131
  count = rules_count
@@ -155,7 +137,7 @@ module Inspec
155
137
  end
156
138
 
157
139
  # iterate over hash of groups
158
- @params[:rules].each { |group, controls|
140
+ params[:rules].each { |group, controls|
159
141
  @logger.info "Verify all controls in #{group}"
160
142
  controls.each { |id, control|
161
143
  sfile, sline = control[:source_location]
@@ -177,21 +159,20 @@ module Inspec
177
159
  end
178
160
 
179
161
  def rules_count
180
- @params[:rules].values.map { |hm| hm.values.length }.inject(:+) || 0
162
+ params[:rules].values.map { |hm| hm.values.length }.inject(:+) || 0
181
163
  end
182
164
 
183
165
  # generates a archive of a folder profile
184
166
  # assumes that the profile was checked before
185
167
  def archive(opts) # rubocop:disable Metrics/AbcSize
186
- profile_name = @params[:name]
187
-
168
+ profile_name = params[:name]
188
169
  ext = opts[:zip] ? 'zip' : 'tar.gz'
189
170
 
190
171
  if opts[:archive]
191
172
  archive = Pathname.new(opts[:archive])
192
173
  else
193
174
  slug = profile_name.downcase.strip.tr(' ', '-').gsub(/[^\w-]/, '_')
194
- archive = Pathname.new(File.dirname(__FILE__)).join('../..', "#{slug}.#{ext}")
175
+ archive = Pathname.new(Dir.pwd).join("#{slug}.#{ext}")
195
176
  end
196
177
 
197
178
  # check if file exists otherwise overwrite the archive
@@ -202,38 +183,68 @@ module Inspec
202
183
 
203
184
  # remove existing archive
204
185
  File.delete(archive) if archive.exist?
205
-
206
186
  @logger.info "Generate archive #{archive}."
207
187
 
208
- # find all files
209
- files = Dir.glob("#{path}/**/*")
210
-
211
188
  # filter files that should not be part of the profile
212
189
  # TODO ignore all .files, but add the files to debug output
213
190
 
214
- # map absolute paths to relative paths
215
- files = files.collect { |f| Pathname.new(f).relative_path_from(Pathname.new(path)).to_s }
216
-
217
191
  # display all files that will be part of the archive
218
192
  @logger.debug 'Add the following files to archive:'
219
- files.each { |f|
220
- @logger.debug ' ' + f
221
- }
193
+ root_path = @source_reader.target.prefix
194
+ files = @source_reader.target.files
195
+ files.each { |f| @logger.debug ' ' + f }
222
196
 
223
197
  if opts[:zip]
224
198
  # generate zip archive
225
199
  require 'inspec/archive/zip'
226
200
  zag = Inspec::Archive::ZipArchiveGenerator.new
227
- zag.archive(path, files, archive)
201
+ zag.archive(root_path, files, archive)
228
202
  else
229
203
  # generate tar archive
230
204
  require 'inspec/archive/tar'
231
205
  tag = Inspec::Archive::TarArchiveGenerator.new
232
- tag.archive(path, files, archive)
206
+ tag.archive(root_path, files, archive)
233
207
  end
234
208
 
235
209
  @logger.info 'Finished archive generation.'
236
210
  true
237
211
  end
212
+
213
+ private
214
+
215
+ def load_params
216
+ params = @source_reader.metadata.params
217
+ params[:name] = @profile_id unless @profile_id.nil?
218
+ params[:rules] = rules = {}
219
+ prefix = @source_reader.target.prefix || ''
220
+
221
+ # we're checking a profile, we don't care if it runs on the host machine
222
+ opts = @options.dup
223
+ opts[:ignore_supports] = true
224
+ runner = Runner.new(
225
+ id: @profile_id,
226
+ backend: :mock,
227
+ test_collector: opts.delete(:test_collector),
228
+ )
229
+ runner.add_profile(self, opts)
230
+
231
+ runner.rules.each do |id, rule|
232
+ file = rule.instance_variable_get(:@__file)
233
+ file = file[prefix.length..-1] if file.start_with?(prefix)
234
+ rules[file] ||= {}
235
+ rules[file][id] = {
236
+ title: rule.title,
237
+ desc: rule.desc,
238
+ impact: rule.impact,
239
+ checks: rule.instance_variable_get(:@checks),
240
+ code: rule.instance_variable_get(:@__code),
241
+ source_location: rule.instance_variable_get(:@__source_location),
242
+ group_title: rule.instance_variable_get(:@__group_title),
243
+ }
244
+ end
245
+
246
+ @profile_id ||= params[:name]
247
+ params
248
+ end
238
249
  end
239
250
  end
@@ -89,6 +89,7 @@ require 'resources/registry_key'
89
89
  require 'resources/script'
90
90
  require 'resources/security_policy'
91
91
  require 'resources/service'
92
+ require 'resources/shadow'
92
93
  require 'resources/ssh_conf'
93
94
  require 'resources/user'
94
95
  require 'resources/windows_feature'
data/lib/inspec/runner.rb CHANGED
@@ -8,12 +8,12 @@ require 'forwardable'
8
8
  require 'uri'
9
9
  require 'inspec/backend'
10
10
  require 'inspec/profile_context'
11
- require 'inspec/targets'
11
+ require 'inspec/profile'
12
12
  require 'inspec/metadata'
13
13
  # spec requirements
14
14
 
15
15
  module Inspec
16
- class Runner # rubocop:disable Metrics/ClassLength
16
+ class Runner
17
17
  extend Forwardable
18
18
  attr_reader :backend, :rules
19
19
  def initialize(conf = {})
@@ -46,45 +46,25 @@ module Inspec
46
46
  @backend = Inspec::Backend.create(@conf)
47
47
  end
48
48
 
49
- def add_test_profile(test, ignore_supports = false)
50
- assets = Inspec::Targets.resolve(test, @conf)
51
- meta_assets = assets.find_all { |a| a[:type] == :metadata }
52
- metas = meta_assets.map do |x|
53
- Inspec::Metadata.from_ref(x[:ref], x[:content], @profile_id, @conf[:logger])
54
- end
55
- metas.each do |meta|
56
- return [] unless ignore_supports || meta.supports_transport?(@backend)
57
- end
58
- assets
59
- end
49
+ def add_profile(profile, options = {})
50
+ return unless options[:ignore_supports] ||
51
+ profile.metadata.supports_transport?(@backend)
60
52
 
61
- def add_tests(tests, options = {})
62
- # retrieve the raw ruby code of all tests
63
- items = tests.map do |test|
64
- add_test_profile(test, options[:ignore_supports])
65
- end.flatten
66
-
67
- tests = items.find_all { |i| i[:type] == :test }
68
- libs = items.find_all { |i| i[:type] == :library }
69
- meta = items.find_all { |i| i[:type] == :metadata }
70
-
71
- # Ensure each test directory exists on the $LOAD_PATH. This
72
- # will ensure traditional RSpec-isms like `require 'spec_helper'`
73
- # continue to work.
74
- tests.flatten.each do |test|
75
- # do not load path for virtual files, eg. from zip
76
- if !test[:ref].nil?
77
- test_directory = File.dirname(test[:ref])
78
- $LOAD_PATH.unshift test_directory unless $LOAD_PATH.include?(test_directory)
79
- end
53
+ libs = profile.libraries.map do |k, v|
54
+ { ref: k, content: v }
80
55
  end
81
56
 
82
- # add all tests (raw) to the runtime
83
- tests.flatten.each do |test|
57
+ profile.tests.each do |ref, content|
58
+ r = profile.source_reader.target.abs_path(ref)
59
+ test = { ref: r, content: content }
84
60
  add_content(test, libs)
85
61
  end
62
+ end
86
63
 
87
- [tests, libs, meta]
64
+ def add_target(target, options = {})
65
+ profile = Inspec::Profile.for_target(target, options)
66
+ fail "Could not resolve #{target} to valid input." if profile.nil?
67
+ add_profile(profile)
88
68
  end
89
69
 
90
70
  def create_context