inspec 0.15.0 → 0.16.0

Sign up to get free protection for your applications and to get access to all the features.
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
  #