inspec 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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