inspec 0.15.0 → 0.16.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 (79) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +40 -2
  3. data/Gemfile +2 -3
  4. data/README.md +2 -0
  5. data/Rakefile +8 -0
  6. data/bin/inspec +1 -157
  7. data/docs/resources.rst +79 -78
  8. data/examples/profile/controls/example.rb +3 -1
  9. data/lib/fetchers/mock.rb +27 -0
  10. data/lib/fetchers/tar.rb +3 -2
  11. data/lib/fetchers/zip.rb +3 -1
  12. data/lib/inspec/cli.rb +164 -0
  13. data/lib/inspec/plugins/resource.rb +6 -2
  14. data/lib/inspec/profile.rb +28 -17
  15. data/lib/inspec/resource.rb +5 -1
  16. data/lib/inspec/rspec_json_formatter.rb +42 -0
  17. data/lib/inspec/rule.rb +24 -1
  18. data/lib/inspec/runner.rb +15 -7
  19. data/lib/inspec/runner_mock.rb +6 -1
  20. data/lib/inspec/runner_rspec.rb +29 -1
  21. data/lib/inspec/version.rb +1 -1
  22. data/lib/resources/{script.rb → powershell.rb} +19 -5
  23. data/lib/resources/registry_key.rb +1 -1
  24. data/test/{integration/cookbooks → cookbooks}/os_prepare/files/empty.iso +0 -0
  25. data/test/{integration/cookbooks → cookbooks}/os_prepare/files/example.csv +0 -0
  26. data/test/{integration/cookbooks → cookbooks}/os_prepare/files/example.ini +0 -0
  27. data/test/{integration/cookbooks → cookbooks}/os_prepare/files/example.json +0 -0
  28. data/test/{integration/cookbooks → cookbooks}/os_prepare/files/example.yml +0 -0
  29. data/test/{integration/cookbooks → cookbooks}/os_prepare/metadata.rb +0 -0
  30. data/test/{integration/cookbooks → cookbooks}/os_prepare/recipes/_runit_service_centos.rb +0 -0
  31. data/test/{integration/cookbooks → cookbooks}/os_prepare/recipes/_upstart_service_centos.rb +0 -0
  32. data/test/{integration/cookbooks → cookbooks}/os_prepare/recipes/apache.rb +0 -0
  33. data/test/{integration/cookbooks → cookbooks}/os_prepare/recipes/apt.rb +0 -0
  34. data/test/{integration/cookbooks → cookbooks}/os_prepare/recipes/auditctl.rb +0 -0
  35. data/test/{integration/cookbooks → cookbooks}/os_prepare/recipes/default.rb +0 -0
  36. data/test/{integration/cookbooks → cookbooks}/os_prepare/recipes/file.rb +0 -0
  37. data/test/{integration/cookbooks → cookbooks}/os_prepare/recipes/iptables.rb +0 -0
  38. data/test/{integration/cookbooks → cookbooks}/os_prepare/recipes/json_yaml_csv_ini.rb +0 -0
  39. data/test/{integration/cookbooks → cookbooks}/os_prepare/recipes/mount.rb +2 -2
  40. data/test/{integration/cookbooks → cookbooks}/os_prepare/recipes/package.rb +0 -0
  41. data/test/{integration/cookbooks → cookbooks}/os_prepare/recipes/postgres.rb +6 -0
  42. data/test/{integration/cookbooks → cookbooks}/os_prepare/recipes/registry_key.rb +0 -0
  43. data/test/{integration/cookbooks → cookbooks}/os_prepare/recipes/service.rb +0 -0
  44. data/test/{integration/cookbooks → cookbooks}/os_prepare/templates/default/sv-default-svlog-run.erb +0 -0
  45. data/test/functional/command_test.rb +390 -0
  46. data/test/helper.rb +6 -0
  47. data/test/integration/{test/integration/default → default}/_debug_spec.rb +0 -0
  48. data/test/integration/{test/integration/default → default}/apache_conf_spec.rb +0 -0
  49. data/test/integration/{test/integration/default → default}/apt_spec.rb +0 -0
  50. data/test/integration/{test/integration/default → default}/auditd_rules_spec.rb +0 -0
  51. data/test/integration/{test/integration/default → default}/compare_matcher_spec.rb +0 -0
  52. data/test/integration/{test/integration/default → default}/csv_spec.rb +0 -0
  53. data/test/integration/{test/integration/default → default}/etc_group_spec.rb +0 -0
  54. data/test/integration/{test/integration/default → default}/file_spec.rb +3 -2
  55. data/test/integration/{test/integration/default → default}/group_spec.rb +0 -0
  56. data/test/integration/{test/integration/default → default}/ini_spec.rb +0 -0
  57. data/test/integration/{test/integration/default → default}/iptables_spec.rb +0 -0
  58. data/test/integration/{test/integration/default → default}/json_spec.rb +0 -0
  59. data/test/integration/{test/integration/default → default}/kernel_module_spec.rb +0 -0
  60. data/test/integration/{test/integration/default → default}/kernel_parameter_spec.rb +0 -0
  61. data/test/integration/{test/integration/default → default}/mount_spec.rb +1 -1
  62. data/test/integration/{test/integration/default → default}/os_spec.rb +0 -0
  63. data/test/integration/{test/integration/default → default}/package_spec.rb +0 -0
  64. data/test/integration/{test/integration/default → default}/port_spec.rb +0 -0
  65. data/test/integration/{test/integration/default → default}/postgres_session_spec.rb +0 -0
  66. data/test/integration/default/powershell_spec.rb +13 -0
  67. data/test/integration/{test/integration/default → default}/registry_key_spec.rb +0 -0
  68. data/test/integration/{test/integration/default → default}/secpol_spec.rb +0 -0
  69. data/test/integration/{test/integration/default → default}/service_spec.rb +0 -0
  70. data/test/integration/{test/integration/default → default}/user_spec.rb +0 -0
  71. data/test/integration/{test/integration/default → default}/yaml_spec.rb +0 -0
  72. data/test/unit/control_test.rb +58 -0
  73. data/test/unit/fetchers/mock_test.rb +43 -0
  74. data/test/unit/plugins/resource_test.rb +60 -0
  75. data/test/unit/resources/{script_test.rb → powershell_test.rb} +10 -1
  76. metadata +107 -101
  77. data/test/integration/.kitchen.ec2.yml +0 -75
  78. data/test/integration/.kitchen.yml +0 -45
  79. data/test/integration/Berksfile +0 -5
@@ -8,7 +8,9 @@ title '/tmp profile'
8
8
  control "tmp-1.0" do # A unique ID for this control
9
9
  impact 0.7 # The criticality, if this control fails.
10
10
  title "Create /tmp directory" # A human-readable title
11
- desc "An optional description..."
11
+ desc "An optional description..." # Describe why this is needed
12
+ ref "Document A-12", url: 'http://...' # Additional references
13
+
12
14
  describe file('/tmp') do # The actual test
13
15
  it { should be_directory }
14
16
  end
@@ -0,0 +1,27 @@
1
+ # encoding: utf-8
2
+ # author: Dominik Richter
3
+ # author: Christoph Hartmann
4
+
5
+ module Fetchers
6
+ class Mock < Inspec.fetcher(1)
7
+ name 'mock'
8
+ priority 0
9
+
10
+ def self.resolve(target)
11
+ return nil unless target.is_a? Hash
12
+ new(target)
13
+ end
14
+
15
+ def initialize(data)
16
+ @data = data
17
+ end
18
+
19
+ def files
20
+ @data.keys
21
+ end
22
+
23
+ def read(file)
24
+ @data[file]
25
+ end
26
+ end
27
+ end
data/lib/fetchers/tar.rb CHANGED
@@ -13,8 +13,9 @@ module Fetchers
13
13
  attr_reader :files
14
14
 
15
15
  def self.resolve(target)
16
- return nil unless File.file?(target)
17
- return nil unless target.end_with?('.tar.gz', '.tgz')
16
+ unless target.is_a?(String) && File.file?(target) && target.end_with?('.tar.gz', '.tgz')
17
+ return nil
18
+ end
18
19
  new(target)
19
20
  end
20
21
 
data/lib/fetchers/zip.rb CHANGED
@@ -12,7 +12,9 @@ module Fetchers
12
12
  attr_reader :files
13
13
 
14
14
  def self.resolve(target)
15
- return nil unless File.file?(target) and target.end_with?('.zip')
15
+ unless target.is_a?(String) && File.file?(target) && target.end_with?('.zip')
16
+ return nil
17
+ end
16
18
  new(target)
17
19
  end
18
20
 
data/lib/inspec/cli.rb ADDED
@@ -0,0 +1,164 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: utf-8
3
+ # Copyright 2015 Dominik Richter. All rights reserved.
4
+ # author: Dominik Richter
5
+ # author: Christoph Hartmann
6
+
7
+ require 'thor'
8
+ require 'json'
9
+ require 'pp'
10
+ require 'utils/base_cli'
11
+ require 'utils/json_log'
12
+
13
+ class Inspec::InspecCLI < Inspec::BaseCLI # rubocop:disable Metrics/ClassLength
14
+ class_option :diagnose, type: :boolean,
15
+ desc: 'Show diagnostics (versions, configurations)'
16
+
17
+ desc 'json PATH', 'read all tests in PATH and generate a JSON summary'
18
+ option :id, type: :string,
19
+ desc: 'Attach a profile ID to all test results'
20
+ option :output, aliases: :o, type: :string,
21
+ desc: 'Save the created profile to a path'
22
+ profile_options
23
+ def json(target)
24
+ diagnose
25
+ o = opts.dup
26
+ o[:ignore_supports] = true
27
+
28
+ profile = Inspec::Profile.for_target(target, o)
29
+ dst = o[:output].to_s
30
+ if dst.empty?
31
+ puts JSON.dump(profile.info)
32
+ else
33
+ if File.exist? dst
34
+ puts "----> updating #{dst}"
35
+ else
36
+ puts "----> creating #{dst}"
37
+ end
38
+ fdst = File.expand_path(dst)
39
+ File.write(fdst, JSON.dump(profile.info))
40
+ end
41
+ end
42
+
43
+ desc 'check PATH', 'verify all tests at the specified PATH'
44
+ option :format, type: :string
45
+ profile_options
46
+ def check(path) # rubocop:disable Metrics/AbcSize
47
+ diagnose
48
+ o = opts.dup
49
+ # configure_logger(o) # we do not need a logger for check yet
50
+ o[:ignore_supports] = true # we check for integrity only
51
+
52
+ # run check
53
+ profile = Inspec::Profile.for_target(path, o)
54
+ result = profile.check
55
+
56
+ if opts['format'] == 'json'
57
+ puts JSON.generate(result)
58
+ else
59
+ headline('Summary')
60
+ %w{location profile controls timestamp valid}.each { |item|
61
+ puts "#{mark_text(item.to_s.capitalize + ':')} #{result[:summary][item.to_sym]}"
62
+ }
63
+ puts
64
+
65
+ %w{errors warnings}.each { |list|
66
+ headline(list.to_s.capitalize)
67
+ result[list.to_sym].each { |item|
68
+ puts "#{item[:file]}:#{item[:line]}:#{item[:column]}: #{item[:msg]} "
69
+ }
70
+ puts
71
+ }
72
+ end
73
+ exit 1 unless result[:summary][:valid]
74
+ end
75
+
76
+ desc 'archive PATH', 'archive a profile to tar.gz (default) or zip'
77
+ profile_options
78
+ option :output, aliases: :o, type: :string,
79
+ desc: 'Save the archive to a path'
80
+ option :zip, type: :boolean, default: false,
81
+ desc: 'Generates a zip archive.'
82
+ option :tar, type: :boolean, default: false,
83
+ desc: 'Generates a tar.gz archive.'
84
+ option :overwrite, type: :boolean, default: false,
85
+ desc: 'Overwrite existing archive.'
86
+ option :ignore_errors, type: :boolean, default: false,
87
+ desc: 'Ignore profile warnings.'
88
+ def archive(path)
89
+ diagnose
90
+
91
+ o = opts.dup
92
+ o[:logger] = Logger.new(STDOUT)
93
+ o[:logger].level = get_log_level(o.log_level)
94
+
95
+ profile = Inspec::Profile.for_target(path, o)
96
+ result = profile.check
97
+
98
+ if result && !opts[:ignore_errors] == false
99
+ @logger.info 'Profile check failed. Please fix the profile before generating an archive.'
100
+ return exit 1
101
+ end
102
+
103
+ # generate archive
104
+ exit 1 unless profile.archive(opts)
105
+ end
106
+
107
+ desc 'exec PATHS', 'run all test files at the specified PATH.'
108
+ exec_options
109
+ def exec(*targets)
110
+ diagnose
111
+ run_tests(targets, opts)
112
+ end
113
+
114
+ desc 'detect', 'detect the target OS'
115
+ target_options
116
+ def detect
117
+ diagnose
118
+
119
+ rel = File.join(File.dirname(__FILE__), *%w{.. utils detect.rb})
120
+ detect_util = File.expand_path(rel)
121
+ # exits on execution:
122
+ runner = Inspec::Runner.new(opts)
123
+ profile = Inspec::Profile.for_target(detect_util, opts)
124
+ runner.add_profile(profile)
125
+ exit runner.run
126
+ rescue RuntimeError => e
127
+ puts e.message
128
+ end
129
+
130
+ desc 'shell', 'open an interactive debugging shell'
131
+ target_options
132
+ option :format, type: :string, default: Inspec::NoSummaryFormatter, hide: true
133
+ def shell_func
134
+ diagnose
135
+ o = opts.dup
136
+ o[:logger] = Logger.new(STDOUT)
137
+ o[:logger].level = get_log_level(o.log_level)
138
+
139
+ runner = Inspec::Runner.new(o)
140
+ Inspec::Shell.new(runner).start
141
+ rescue RuntimeError => e
142
+ puts e.message
143
+ end
144
+
145
+ desc 'version', 'prints the version of this tool'
146
+ def version
147
+ puts Inspec::VERSION
148
+ end
149
+ end
150
+
151
+ # Load all plugins on startup
152
+ ctl = Inspec::PluginCtl.new
153
+ ctl.list.each { |x| ctl.load(x) }
154
+
155
+ # load CLI plugins before the Inspec CLI has been started
156
+ Inspec::Plugins::CLI.subcommands.each { |_subcommand, params|
157
+ Inspec::InspecCLI.register(
158
+ params[:klass],
159
+ params[:subcommand_name],
160
+ params[:usage],
161
+ params[:description],
162
+ params[:options],
163
+ )
164
+ }
@@ -2,6 +2,8 @@
2
2
  # author: Dominik Richter
3
3
  # author: Christoph Hartmann
4
4
 
5
+ require 'inspec/resource'
6
+
5
7
  module Inspec
6
8
  module Plugins
7
9
  class Resource
@@ -50,8 +52,10 @@ module Inspec
50
52
  end
51
53
  # rubocop:enable Lint/NestedMethodDefinition
52
54
 
53
- # add the resource to the registry by name
54
- Inspec::Resource.registry[name] = cl
55
+ # add the resource to the registry by name with a newly-named registry class
56
+ klass_name = name.split('_').map(&:capitalize).join
57
+ Inspec::Resource::Registry.const_set(klass_name, cl)
58
+ Inspec::Resource.registry[name] = Inspec::Resource::Registry.const_get(klass_name)
55
59
  end
56
60
 
57
61
  # Define methods which are available to all resources
@@ -173,26 +173,17 @@ module Inspec
173
173
 
174
174
  # generates a archive of a folder profile
175
175
  # assumes that the profile was checked before
176
- def archive(opts) # rubocop:disable Metrics/AbcSize
177
- profile_name = params[:name]
178
- ext = opts[:zip] ? 'zip' : 'tar.gz'
179
-
180
- if opts[:archive]
181
- archive = Pathname.new(opts[:archive])
182
- else
183
- slug = profile_name.downcase.strip.tr(' ', '-').gsub(/[^\w-]/, '_')
184
- archive = Pathname.new(Dir.pwd).join("#{slug}.#{ext}")
185
- end
186
-
176
+ def archive(opts)
187
177
  # check if file exists otherwise overwrite the archive
188
- if archive.exist? && !opts[:overwrite]
189
- @logger.info "Archive #{archive} exists already. Use --overwrite."
178
+ dst = archive_name(opts)
179
+ if dst.exist? && !opts[:overwrite]
180
+ @logger.info "Archive #{dst} exists already. Use --overwrite."
190
181
  return false
191
182
  end
192
183
 
193
184
  # remove existing archive
194
- File.delete(archive) if archive.exist?
195
- @logger.info "Generate archive #{archive}."
185
+ File.delete(dst) if dst.exist?
186
+ @logger.info "Generate archive #{dst}."
196
187
 
197
188
  # filter files that should not be part of the profile
198
189
  # TODO ignore all .files, but add the files to debug output
@@ -207,12 +198,12 @@ module Inspec
207
198
  # generate zip archive
208
199
  require 'inspec/archive/zip'
209
200
  zag = Inspec::Archive::ZipArchiveGenerator.new
210
- zag.archive(root_path, files, archive)
201
+ zag.archive(root_path, files, dst)
211
202
  else
212
203
  # generate tar archive
213
204
  require 'inspec/archive/tar'
214
205
  tag = Inspec::Archive::TarArchiveGenerator.new
215
- tag.archive(root_path, files, archive)
206
+ tag.archive(root_path, files, dst)
216
207
  end
217
208
 
218
209
  @logger.info 'Finished archive generation.'
@@ -221,6 +212,24 @@ module Inspec
221
212
 
222
213
  private
223
214
 
215
+ # Create an archive name for this profile and an additional options
216
+ # configuration. Either use :output or generate the name from metadata.
217
+ #
218
+ # @param [Hash] configuration options
219
+ # @return [Pathname] path for the archive
220
+ def archive_name(opts)
221
+ if (name = opts[:output])
222
+ return Pathname.new(name)
223
+ end
224
+
225
+ name = params[:name] ||
226
+ fail('Cannot create an archive without a profile name! Please '\
227
+ 'specify the name in metadata or use --output to create the archive.')
228
+ ext = opts[:zip] ? 'zip' : 'tar.gz'
229
+ slug = name.downcase.strip.tr(' ', '-').gsub(/[^\w-]/, '_')
230
+ Pathname.new(Dir.pwd).join("#{slug}.#{ext}")
231
+ end
232
+
224
233
  def load_params
225
234
  params = @source_reader.metadata.params
226
235
  params[:name] = @profile_id unless @profile_id.nil?
@@ -245,6 +254,8 @@ module Inspec
245
254
  title: rule.title,
246
255
  desc: rule.desc,
247
256
  impact: rule.impact,
257
+ refs: rule.ref,
258
+ tags: rule.tag,
248
259
  checks: rule.instance_variable_get(:@checks),
249
260
  code: rule.instance_variable_get(:@__code),
250
261
  source_location: rule.instance_variable_get(:@__source_location),
@@ -8,6 +8,10 @@ require 'inspec/plugins'
8
8
 
9
9
  module Inspec
10
10
  class Resource
11
+ class Registry
12
+ # empty class for namespacing resource classes in the registry
13
+ end
14
+
11
15
  def self.registry
12
16
  @registry ||= {}
13
17
  end
@@ -84,9 +88,9 @@ require 'resources/port'
84
88
  require 'resources/postgres'
85
89
  require 'resources/postgres_conf'
86
90
  require 'resources/postgres_session'
91
+ require 'resources/powershell'
87
92
  require 'resources/processes'
88
93
  require 'resources/registry_key'
89
- require 'resources/script'
90
94
  require 'resources/security_policy'
91
95
  require 'resources/service'
92
96
  require 'resources/shadow'
@@ -3,6 +3,7 @@
3
3
  # author: Christoph Hartmann
4
4
 
5
5
  require 'rspec/core'
6
+ require 'rspec/core/formatters/json_formatter'
6
7
 
7
8
  # Extend the basic RSpec JSON Formatter
8
9
  # to give us an ID in its output
@@ -26,3 +27,44 @@ module RSpec::Core::Formatters
26
27
  end
27
28
  end
28
29
  end
30
+
31
+ class InspecRspecFormatter < RSpec::Core::Formatters::JsonFormatter
32
+ RSpec::Core::Formatters.register self, :message, :dump_summary, :dump_profile, :stop, :close
33
+
34
+ def add_profile(profile)
35
+ @profiles ||= []
36
+ @profiles.push(profile)
37
+ end
38
+
39
+ def dump_summary(summary)
40
+ super(summary)
41
+ @output_hash[:profiles] = @profiles.map do |profile|
42
+ r = profile.params.dup
43
+ r.delete(:rules)
44
+ r
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def format_example(example)
51
+ res = {
52
+ id: example.metadata[:id],
53
+ title: example.metadata[:title],
54
+ desc: example.metadata[:desc],
55
+ code: example.metadata[:code],
56
+ impact: example.metadata[:impact],
57
+ status: example.execution_result.status.to_s,
58
+ code_desc: example.full_description,
59
+ ref: example.metadata['file_path'],
60
+ ref_line: example.metadata['line_number'],
61
+ run_time: example.execution_result.run_time,
62
+ start_time: example.execution_result.started_at.to_s,
63
+ }
64
+
65
+ # pending messages are embedded in the resources description
66
+ res[:pending] = example.metadata[:description] if res[:status] == 'pending'
67
+
68
+ res
69
+ end
70
+ end
data/lib/inspec/rule.rb CHANGED
@@ -9,7 +9,7 @@ require 'inspec/describe'
9
9
  require 'inspec/expect'
10
10
 
11
11
  module Inspec
12
- class Rule
12
+ class Rule # rubocop:disable Metrics/ClassLength
13
13
  include ::RSpec::Matchers
14
14
 
15
15
  def initialize(id, _opts, &block)
@@ -20,6 +20,8 @@ module Inspec
20
20
  @__source_location = __get_block_source_location(&block)
21
21
  @title = nil
22
22
  @desc = nil
23
+ @refs = []
24
+ @tags = {}
23
25
  # not changeable by the user:
24
26
  @profile_id = nil
25
27
  @checks = []
@@ -47,6 +49,27 @@ module Inspec
47
49
  @desc
48
50
  end
49
51
 
52
+ def ref(ref = nil, opts = {})
53
+ return @refs if ref.nil? && opts.empty?
54
+ if opts.empty? && ref.is_a?(Hash)
55
+ opts = ref
56
+ else
57
+ opts[:ref] = ref
58
+ end
59
+ @refs.push(opts)
60
+ end
61
+
62
+ def tag(*args)
63
+ args.each do |arg|
64
+ if arg.is_a?(Hash)
65
+ @tags.merge!(arg)
66
+ else
67
+ @tags[arg] ||= nil
68
+ end
69
+ end
70
+ @tags
71
+ end
72
+
50
73
  # Describe will add one or more tests to this control. There is 2 ways
51
74
  # of calling it:
52
75
  #