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
@@ -0,0 +1,38 @@
1
+ # encoding: utf-8
2
+ # author: Dominik Richter
3
+ # author: Christoph Hartmann
4
+
5
+ require 'inspec/fetcher'
6
+ require 'inspec/metadata'
7
+
8
+ module SourceReaders
9
+ class Flat < Inspec.source_reader(1)
10
+ name 'flat'
11
+ priority 5
12
+
13
+ def self.resolve(target)
14
+ # TODO: eventually remove the metadata.rb exception here
15
+ # when we have fully phased out metadata.rb in 1.0
16
+ files = target.files.find_all { |x|
17
+ x.end_with?('.rb') && !x.include?('/') && x != 'metadata.rb'
18
+ }
19
+ return nil if files.empty?
20
+ new(target, files)
21
+ end
22
+
23
+ attr_reader :metadata, :tests, :libraries
24
+
25
+ def initialize(target, files)
26
+ @target = target
27
+ @metadata = ::Inspec::Metadata.new(nil)
28
+ @tests = load_tests(files)
29
+ @libraries = {}
30
+ end
31
+
32
+ private
33
+
34
+ def load_tests(files)
35
+ Hash[files.map { |x| [x, @target.read(x)] }]
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,78 @@
1
+ # encoding: utf-8
2
+ # author: Dominik Richter
3
+ # author: Christoph Hartmann
4
+
5
+ require 'inspec/fetcher'
6
+ require 'inspec/metadata'
7
+
8
+ module SourceReaders
9
+ class InspecReader < Inspec.source_reader(1)
10
+ name 'inspec'
11
+ priority 10
12
+
13
+ def self.resolve(target)
14
+ return new(target, 'inspec.yml') if target.files.include?('inspec.yml')
15
+ # TODO: deprecated for 1.0.0 release
16
+ if target.files.include?('metadata.rb') &&
17
+ (
18
+ target.files.include?('controls') ||
19
+ target.files.include?('test')
20
+ )
21
+ return new(target, 'metadata.rb')
22
+ end
23
+ nil
24
+ end
25
+
26
+ attr_reader :metadata, :tests, :libraries
27
+
28
+ def initialize(target, metadata_source)
29
+ @target = target
30
+ @metadata = Inspec::Metadata.from_ref(
31
+ metadata_source,
32
+ @target.read(metadata_source),
33
+ nil)
34
+
35
+ @tests = load_tests
36
+ @libraries = load_libs
37
+ prepare_load_paths
38
+ end
39
+
40
+ private
41
+
42
+ def load_tests
43
+ tests = @target.files.find_all do |path|
44
+ path.start_with?('controls', 'test') && path.end_with?('.rb')
45
+ end
46
+ Hash[tests.map { |x| [x, @target.read(x)] }]
47
+ end
48
+
49
+ def load_libs
50
+ tests = @target.files.find_all do |path|
51
+ path.start_with?('libraries') && path.end_with?('.rb')
52
+ end
53
+ Hash[tests.map { |x| [x, @target.read(x)] }]
54
+ end
55
+
56
+ # Ensure each test directory exists on the $LOAD_PATH. This
57
+ # will ensure traditional RSpec-isms like `require 'spec_helper'`
58
+ # continue to work. The method outlined here is only meant to be temporary!
59
+ def prepare_load_paths
60
+ t = @target
61
+ t = @target.parent unless @target.parent.nil?
62
+ unless t.is_a?(Fetchers::Local)
63
+ return # no need to mess with load-paths if this is not on disk
64
+ end
65
+
66
+ rel_dirs = (@libraries.keys + @tests.keys)
67
+ .map { |x| File.dirname(x) }.uniq
68
+
69
+ abs_dirs = rel_dirs.map { |x| @target.abs_path(x) }
70
+ .find_all { |x| File.directory?(x) }
71
+ .map { |x| File.expand_path(x) }
72
+
73
+ abs_dirs.each do |dir|
74
+ $LOAD_PATH.unshift dir unless $LOAD_PATH.include?(dir)
75
+ end
76
+ end
77
+ end
78
+ end
@@ -42,13 +42,13 @@ module Inspec
42
42
  private
43
43
 
44
44
  # helper method to run tests
45
- def run_tests(opts, tests)
45
+ def run_tests(targets, opts)
46
46
  o = opts.dup
47
47
  o[:logger] = Logger.new(opts['format'] == 'json' ? nil : STDOUT)
48
48
  o[:logger].level = get_log_level(o.log_level)
49
49
 
50
50
  runner = Inspec::Runner.new(o)
51
- runner.add_tests(tests)
51
+ targets.each { |target| runner.add_target(target, opts) }
52
52
  exit runner.run
53
53
  rescue RuntimeError => e
54
54
  puts e.message
data/lib/utils/parser.rb CHANGED
@@ -20,7 +20,7 @@ module PasswdParser
20
20
  def parse_passwd_line(line)
21
21
  x = line.split(':')
22
22
  {
23
- 'name' => x.at(0),
23
+ 'user' => x.at(0),
24
24
  'password' => x.at(1),
25
25
  'uid' => x.at(2),
26
26
  'gid' => x.at(3),
@@ -0,0 +1,93 @@
1
+ # encoding: utf-8
2
+ # author: Dominik Richter
3
+ # author: Christoph Hartmann
4
+
5
+ class PluginRegistry
6
+ attr_reader :registry
7
+
8
+ def initialize
9
+ @registry = {}
10
+ end
11
+
12
+ # Resolve a target via available plugins.
13
+ #
14
+ # @param [String] target to resolve
15
+ # @return [Plugin] plugin instance if it can be resolved, nil otherwise
16
+ def resolve(target)
17
+ modules.each do |m|
18
+ res = m.resolve(target)
19
+ return res unless res.nil?
20
+ end
21
+ nil
22
+ end
23
+
24
+ private
25
+
26
+ # Get all registered plugins sorted by priority, with highest first
27
+ #
28
+ # @return [Array[Plugin]] sorted list of plugins
29
+ def modules
30
+ @registry.values
31
+ .sort_by { |x| x.respond_to?(:priority) ? x.priority : 0 }
32
+ .reverse
33
+ end
34
+ end
35
+
36
+ class PluginRegistry
37
+ class Plugin
38
+ # Retrieve the plugin registry associated with this plugin
39
+ #
40
+ # @return [PluginRegistry] plugin registry for this plugin
41
+ def self.plugin_registry
42
+ fail "Plugin #{self} does not implement `self.plugin_registry()`. This method is required"
43
+ end
44
+
45
+ # Register a new plugin by name
46
+ #
47
+ # @param [String] the unique name of this plugin
48
+ # @return [nil] disregard
49
+ def self.name(name)
50
+ fail "Trying to register #{self} with name == nil" if name.nil?
51
+ @name = name
52
+ plugin_registry.registry[name] = self
53
+ end
54
+
55
+ # This plugin's priority. Set it by providing the priority as an
56
+ # argument. Higher numbers ensure that the plugin is
57
+ # called early to check if a target belongs to it. When called without
58
+ # an argument, it retrieves this plugin's priority. Defaults to 0.
59
+ #
60
+ # @param [Numeric] Priority as a number. Will only be set if != nil
61
+ # @return [Numeric] This plugin's priority
62
+ def self.priority(x = nil)
63
+ @priority = x unless x.nil?
64
+ @priority || 0
65
+ end
66
+
67
+ # Try to resolve the target. If this plugin cannot handle it, the result
68
+ # will be nil. If, however, the plugin can resolve it, the resulting
69
+ # object will be an instance of this plugin. This means, that the interface
70
+ # that this base class provides, is the basis for the returned type.
71
+ #
72
+ # @param [String] target to try to resolve
73
+ # @return [Plugin] instance if it can be resolved, nil otherwise
74
+ def self.resolve(_target)
75
+ fail "Plugin #{self} does not implement `self.resolve(target)`. This method is required"
76
+ end
77
+
78
+ # When a plugin's resolve doesn't lead to the final state, it can
79
+ # use this method to hand it back for another resolver to handle.
80
+ #
81
+ # @param [Any] the current target that needs resolving
82
+ # @param [Plugin] an instance of the calling resolver
83
+ # @return [Plugin] instance if it can be resolved, nil otherwise
84
+ def self.resolve_next(target, parent)
85
+ res = plugin_registry.resolve(target)
86
+ res.parent = parent
87
+ res
88
+ end
89
+
90
+ attr_reader :target
91
+ attr_accessor :parent
92
+ end
93
+ end
data/test/docker_test.rb CHANGED
@@ -42,7 +42,7 @@ class DockerTester
42
42
  puts "--> run test on docker #{container.id}"
43
43
  opts = { 'target' => "docker://#{container.id}" }
44
44
  runner = Inspec::Runner.new(opts)
45
- runner.add_tests(@tests)
45
+ @tests.each { |test| runner.add_target(test, opts) }
46
46
  runner.tests.map { |g| g.run(report) }
47
47
  end
48
48
  end
data/test/helper.rb CHANGED
@@ -14,12 +14,19 @@ SimpleCov.start do
14
14
  add_group 'Backends', 'lib/inspec/backend'
15
15
  end
16
16
 
17
+ require 'fileutils'
18
+ require 'pathname'
19
+ require 'tempfile'
20
+ require 'zip'
21
+
17
22
  require 'utils/base_cli'
18
- require 'inspec/targets'
23
+ require 'inspec/fetcher'
24
+ require 'inspec/source_reader'
19
25
  require 'inspec/resource'
20
26
  require 'inspec/backend'
21
27
  require 'inspec/profile'
22
-
28
+ require 'inspec/runner'
29
+ require 'inspec/runner_mock'
23
30
 
24
31
  class MockLoader
25
32
  # collects emulation operating systems
@@ -44,6 +51,8 @@ class MockLoader
44
51
  undefined: { family: nil, release: nil, arch: nil },
45
52
  }
46
53
 
54
+ @archives = {}
55
+
47
56
  # pass the os identifier to emulate a specific operating system
48
57
  def initialize(os = nil)
49
58
  # selects operating system
@@ -86,6 +95,7 @@ class MockLoader
86
95
  '/etc/ssh/ssh_config' => mockfile.call('ssh_config'),
87
96
  '/etc/ssh/sshd_config' => mockfile.call('sshd_config'),
88
97
  '/etc/passwd' => mockfile.call('passwd'),
98
+ '/etc/shadow' => mockfile.call('shadow'),
89
99
  '/etc/ntp.conf' => mockfile.call('ntp.conf'),
90
100
  '/etc/login.defs' => mockfile.call('login.defs'),
91
101
  '/etc/security/limits.conf' => mockfile.call('limits.conf'),
@@ -238,6 +248,56 @@ class MockLoader
238
248
  resource.inspec.backend
239
249
  .mock_command(cmd, res[:stdout], res[:stderr], res[:exit_status])
240
250
  end
251
+
252
+ def self.home
253
+ File.join(File.dirname(__FILE__), 'unit')
254
+ end
255
+
256
+ def self.profile_path(name)
257
+ dst = name
258
+ dst = "#{home}/mock/profiles/#{name}" unless name.start_with?(home)
259
+ dst
260
+ end
261
+
262
+ def self.load_profile(name, opts = {})
263
+ opts[:test_collector] = Inspec::RunnerMock.new
264
+ Inspec::Profile.for_target(profile_path(name), opts)
265
+ end
266
+
267
+ def self.profile_tgz(name)
268
+ path = File.join(home, 'mock', 'profiles', name)
269
+ archive = Tempfile.new([name, '.tar.gz'])
270
+ dst = archive.path
271
+ archive.close
272
+
273
+ # generate relative paths
274
+ files = Dir.glob("#{path}/**/*")
275
+ relatives = files.map { |e| Pathname.new(e).relative_path_from(Pathname.new(path)).to_s }
276
+
277
+ require 'inspec/archive/tar'
278
+ tag = Inspec::Archive::TarArchiveGenerator.new
279
+ tag.archive(path, relatives, dst)
280
+ @archives[dst] = archive
281
+
282
+ dst
283
+ end
284
+
285
+ def self.profile_zip(name, opts = {})
286
+ path = File.join(home, 'mock', 'profiles', name)
287
+ archive = Tempfile.new([name, '.zip'])
288
+ dst = archive.path
289
+ archive.close
290
+
291
+ # rubyzip only works relative paths
292
+ files = Dir.glob("#{path}/**/*")
293
+ relatives = files.map { |e| Pathname.new(e).relative_path_from(Pathname.new(path)).to_s }
294
+
295
+ require 'inspec/archive/zip'
296
+ zag = Inspec::Archive::ZipArchiveGenerator.new
297
+ zag.archive(path, relatives, dst)
298
+ @archives[dst] = archive
299
+ dst
300
+ end
241
301
  end
242
302
 
243
303
  def load_resource(*args)
@@ -12,6 +12,8 @@ when 'ubuntu'
12
12
 
13
13
  when 'centos'
14
14
  # install runit for alternative service mgmt
15
- include_recipe 'os_prepare::_runit_service_centos'
16
- include_recipe 'os_prepare::_upstart_service_centos'
15
+ if node['platform_version'].to_i >= 6
16
+ include_recipe 'os_prepare::_runit_service_centos'
17
+ include_recipe 'os_prepare::_upstart_service_centos'
18
+ end
17
19
  end
@@ -18,4 +18,15 @@ if os.linux?
18
18
  its('LogLevel') { should cmp 'info' }
19
19
  its('LogLevel') { should cmp 'InfO' }
20
20
  end
21
+
22
+ describe passwd.passwords.uniq do
23
+ it { should eq ['x'] }
24
+ it { should cmp ['x'] }
25
+ it { should cmp 'x' }
26
+ end
27
+
28
+ describe passwd.usernames do
29
+ it { should include 'root' }
30
+ it { should_not cmp 'root' }
31
+ end
21
32
  end
@@ -47,7 +47,7 @@ if os[:family] == 'ubuntu'
47
47
  end
48
48
 
49
49
  # extra tests for alt. runit on centos with runit_service
50
- if os[:family] == 'centos'
50
+ if os[:family] == 'centos' && os[:release].to_i >= 6
51
51
  describe runit_service('running-runit-service') do
52
52
  it { should be_enabled }
53
53
  it { should be_installed }
@@ -103,3 +103,18 @@ if os[:family] == 'centos'
103
103
  it { should_not be_running }
104
104
  end
105
105
  end
106
+
107
+ # extra tests for sys-v runlevels
108
+ if os[:family] == 'centos' && os[:release].to_i <= 6
109
+ describe service('sshd').runlevels do
110
+ its('keys') { should include(2) }
111
+ end
112
+
113
+ describe service('sshd').runlevels(2, 4) do
114
+ it { should be_enabled }
115
+ end
116
+
117
+ describe service('sshd').runlevels(0, 1) do
118
+ it { should_not be_enabled }
119
+ end
120
+ end
@@ -0,0 +1,61 @@
1
+ # encoding: utf-8
2
+ # author: Dominik Richter
3
+ # author: Christoph Hartmann
4
+
5
+ require 'helper'
6
+
7
+ describe Inspec::Fetcher do
8
+ it 'loads the local fetcher for this file' do
9
+ res = Inspec::Fetcher.resolve(__FILE__)
10
+ res.must_be_kind_of Fetchers::Local
11
+ end
12
+ end
13
+
14
+ describe Inspec::Plugins::RelFetcher do
15
+ def fetcher
16
+ src_fetcher.expects(:files).returns(in_files).at_least_once
17
+ Inspec::Plugins::RelFetcher.new(src_fetcher)
18
+ end
19
+
20
+ let(:src_fetcher) { mock() }
21
+
22
+ IN_AND_OUT = {
23
+ [] => [],
24
+ %w{file} => %w{file},
25
+ # don't prefix just by filename
26
+ %w{file file_a} => %w{file file_a},
27
+ %w{path/file path/file_a} => %w{file file_a},
28
+ %w{path/to/file} => %w{file},
29
+ %w{/path/to/file} => %w{file},
30
+ %w{alice bob} => %w{alice bob},
31
+ # mixed paths
32
+ %w{x/a y/b} => %w{x/a y/b},
33
+ %w{/x/a /y/b} => %w{x/a y/b},
34
+ %w{z/x/a z/y/b} => %w{x/a y/b},
35
+ %w{/z/x/a /z/y/b} => %w{x/a y/b},
36
+ # mixed with relative path
37
+ %w{a path/to/b} => %w{a path/to/b},
38
+ %w{path/to/b a} => %w{path/to/b a},
39
+ %w{path/to/b path/a} => %w{to/b a},
40
+ %w{path/to/b path/a c} => %w{path/to/b path/a c},
41
+ # mixed with absolute paths
42
+ %w{/path/to/b /a} => %w{path/to/b a},
43
+ %w{/path/to/b /path/a} => %w{to/b a},
44
+ %w{/path/to/b /path/a /c} => %w{path/to/b path/a c},
45
+ # mixing absolute and relative paths
46
+ %w{path/a /path/b} => %w{path/a /path/b},
47
+ %w{/path/a path/b} => %w{/path/a path/b},
48
+ # extract folder structure buildup
49
+ %w{/a /a/b /a/b/c} => %w{c},
50
+ %w{/a /a/b /a/b/c/d/e} => %w{e},
51
+ }.each do |ins, outs|
52
+ describe 'empty profile' do
53
+ let(:in_files) { ins }
54
+
55
+ it 'also has no files' do
56
+ fetcher.files.must_equal outs
57
+ end
58
+ end
59
+ end
60
+
61
+ end