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,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