inspec 0.15.0 → 0.16.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +40 -2
- data/Gemfile +2 -3
- data/README.md +2 -0
- data/Rakefile +8 -0
- data/bin/inspec +1 -157
- data/docs/resources.rst +79 -78
- data/examples/profile/controls/example.rb +3 -1
- data/lib/fetchers/mock.rb +27 -0
- data/lib/fetchers/tar.rb +3 -2
- data/lib/fetchers/zip.rb +3 -1
- data/lib/inspec/cli.rb +164 -0
- data/lib/inspec/plugins/resource.rb +6 -2
- data/lib/inspec/profile.rb +28 -17
- data/lib/inspec/resource.rb +5 -1
- data/lib/inspec/rspec_json_formatter.rb +42 -0
- data/lib/inspec/rule.rb +24 -1
- data/lib/inspec/runner.rb +15 -7
- data/lib/inspec/runner_mock.rb +6 -1
- data/lib/inspec/runner_rspec.rb +29 -1
- data/lib/inspec/version.rb +1 -1
- data/lib/resources/{script.rb → powershell.rb} +19 -5
- data/lib/resources/registry_key.rb +1 -1
- data/test/{integration/cookbooks → cookbooks}/os_prepare/files/empty.iso +0 -0
- data/test/{integration/cookbooks → cookbooks}/os_prepare/files/example.csv +0 -0
- data/test/{integration/cookbooks → cookbooks}/os_prepare/files/example.ini +0 -0
- data/test/{integration/cookbooks → cookbooks}/os_prepare/files/example.json +0 -0
- data/test/{integration/cookbooks → cookbooks}/os_prepare/files/example.yml +0 -0
- data/test/{integration/cookbooks → cookbooks}/os_prepare/metadata.rb +0 -0
- data/test/{integration/cookbooks → cookbooks}/os_prepare/recipes/_runit_service_centos.rb +0 -0
- data/test/{integration/cookbooks → cookbooks}/os_prepare/recipes/_upstart_service_centos.rb +0 -0
- data/test/{integration/cookbooks → cookbooks}/os_prepare/recipes/apache.rb +0 -0
- data/test/{integration/cookbooks → cookbooks}/os_prepare/recipes/apt.rb +0 -0
- data/test/{integration/cookbooks → cookbooks}/os_prepare/recipes/auditctl.rb +0 -0
- data/test/{integration/cookbooks → cookbooks}/os_prepare/recipes/default.rb +0 -0
- data/test/{integration/cookbooks → cookbooks}/os_prepare/recipes/file.rb +0 -0
- data/test/{integration/cookbooks → cookbooks}/os_prepare/recipes/iptables.rb +0 -0
- data/test/{integration/cookbooks → cookbooks}/os_prepare/recipes/json_yaml_csv_ini.rb +0 -0
- data/test/{integration/cookbooks → cookbooks}/os_prepare/recipes/mount.rb +2 -2
- data/test/{integration/cookbooks → cookbooks}/os_prepare/recipes/package.rb +0 -0
- data/test/{integration/cookbooks → cookbooks}/os_prepare/recipes/postgres.rb +6 -0
- data/test/{integration/cookbooks → cookbooks}/os_prepare/recipes/registry_key.rb +0 -0
- data/test/{integration/cookbooks → cookbooks}/os_prepare/recipes/service.rb +0 -0
- data/test/{integration/cookbooks → cookbooks}/os_prepare/templates/default/sv-default-svlog-run.erb +0 -0
- data/test/functional/command_test.rb +390 -0
- data/test/helper.rb +6 -0
- data/test/integration/{test/integration/default → default}/_debug_spec.rb +0 -0
- data/test/integration/{test/integration/default → default}/apache_conf_spec.rb +0 -0
- data/test/integration/{test/integration/default → default}/apt_spec.rb +0 -0
- data/test/integration/{test/integration/default → default}/auditd_rules_spec.rb +0 -0
- data/test/integration/{test/integration/default → default}/compare_matcher_spec.rb +0 -0
- data/test/integration/{test/integration/default → default}/csv_spec.rb +0 -0
- data/test/integration/{test/integration/default → default}/etc_group_spec.rb +0 -0
- data/test/integration/{test/integration/default → default}/file_spec.rb +3 -2
- data/test/integration/{test/integration/default → default}/group_spec.rb +0 -0
- data/test/integration/{test/integration/default → default}/ini_spec.rb +0 -0
- data/test/integration/{test/integration/default → default}/iptables_spec.rb +0 -0
- data/test/integration/{test/integration/default → default}/json_spec.rb +0 -0
- data/test/integration/{test/integration/default → default}/kernel_module_spec.rb +0 -0
- data/test/integration/{test/integration/default → default}/kernel_parameter_spec.rb +0 -0
- data/test/integration/{test/integration/default → default}/mount_spec.rb +1 -1
- data/test/integration/{test/integration/default → default}/os_spec.rb +0 -0
- data/test/integration/{test/integration/default → default}/package_spec.rb +0 -0
- data/test/integration/{test/integration/default → default}/port_spec.rb +0 -0
- data/test/integration/{test/integration/default → default}/postgres_session_spec.rb +0 -0
- data/test/integration/default/powershell_spec.rb +13 -0
- data/test/integration/{test/integration/default → default}/registry_key_spec.rb +0 -0
- data/test/integration/{test/integration/default → default}/secpol_spec.rb +0 -0
- data/test/integration/{test/integration/default → default}/service_spec.rb +0 -0
- data/test/integration/{test/integration/default → default}/user_spec.rb +0 -0
- data/test/integration/{test/integration/default → default}/yaml_spec.rb +0 -0
- data/test/unit/control_test.rb +58 -0
- data/test/unit/fetchers/mock_test.rb +43 -0
- data/test/unit/plugins/resource_test.rb +60 -0
- data/test/unit/resources/{script_test.rb → powershell_test.rb} +10 -1
- metadata +107 -101
- data/test/integration/.kitchen.ec2.yml +0 -75
- data/test/integration/.kitchen.yml +0 -45
- 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
|
-
|
17
|
-
|
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
|
-
|
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
|
-
|
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
|
data/lib/inspec/profile.rb
CHANGED
@@ -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)
|
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
|
-
|
189
|
-
|
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(
|
195
|
-
@logger.info "Generate 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,
|
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,
|
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),
|
data/lib/inspec/resource.rb
CHANGED
@@ -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
|
#
|