inspec 0.20.1 → 0.21.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +45 -2
- data/docs/dsl_inspec.rst +2 -2
- data/docs/resources.rst +9 -9
- data/docs/ruby_usage.rst +145 -0
- data/inspec.gemspec +1 -0
- data/lib/bundles/inspec-compliance/cli.rb +15 -2
- data/lib/inspec/cli.rb +23 -10
- data/lib/inspec/dsl.rb +0 -52
- data/lib/inspec/objects/or_test.rb +1 -0
- data/lib/inspec/objects/test.rb +4 -4
- data/lib/inspec/profile.rb +76 -61
- data/lib/inspec/profile_context.rb +12 -11
- data/lib/inspec/rspec_json_formatter.rb +93 -40
- data/lib/inspec/rule.rb +7 -29
- data/lib/inspec/runner.rb +15 -4
- data/lib/inspec/runner_mock.rb +1 -1
- data/lib/inspec/runner_rspec.rb +26 -24
- data/lib/inspec/version.rb +1 -1
- data/lib/matchers/matchers.rb +3 -3
- data/lib/resources/auditd_rules.rb +2 -2
- data/lib/resources/host.rb +1 -1
- data/lib/resources/interface.rb +1 -1
- data/lib/resources/kernel_parameter.rb +1 -1
- data/lib/resources/mount.rb +2 -1
- data/lib/resources/mysql_session.rb +1 -1
- data/lib/resources/os_env.rb +2 -2
- data/lib/resources/passwd.rb +33 -93
- data/lib/resources/port.rb +47 -3
- data/lib/resources/processes.rb +3 -3
- data/lib/resources/service.rb +33 -1
- data/lib/resources/user.rb +15 -15
- data/lib/utils/base_cli.rb +1 -3
- data/lib/utils/filter.rb +30 -7
- data/test/cookbooks/os_prepare/recipes/_upstart_service_centos.rb +4 -0
- data/test/functional/helper.rb +1 -0
- data/test/functional/inheritance_test.rb +1 -1
- data/test/functional/inspec_compliance_test.rb +4 -3
- data/test/functional/inspec_exec_json_test.rb +122 -0
- data/test/functional/inspec_exec_test.rb +23 -117
- data/test/functional/{inspec_json_test.rb → inspec_json_profile_test.rb} +13 -15
- data/test/functional/inspec_test.rb +15 -2
- data/test/helper.rb +5 -1
- data/test/integration/default/auditd_rules_spec.rb +3 -3
- data/test/integration/default/kernel_parameter_spec.rb +6 -6
- data/test/integration/default/service_spec.rb +4 -0
- data/test/resource/command_test.rb +9 -9
- data/test/resource/dsl_test.rb +1 -1
- data/test/resource/file_test.rb +17 -17
- data/test/unit/control_test.rb +1 -1
- data/test/unit/mock/cmd/hpux-netstat-inet +10 -0
- data/test/unit/mock/cmd/hpux-netstat-inet6 +11 -0
- data/test/unit/mock/profiles/skippy-profile-os/controls/one.rb +1 -1
- data/test/unit/profile_context_test.rb +2 -2
- data/test/unit/profile_test.rb +11 -14
- data/test/unit/resources/passwd_test.rb +13 -14
- data/test/unit/resources/port_test.rb +14 -0
- data/test/unit/resources/processes_test.rb +3 -3
- data/test/unit/resources/service_test.rb +103 -39
- data/test/unit/utils/filter_table_test.rb +35 -3
- metadata +25 -4
data/lib/inspec/objects/test.rb
CHANGED
@@ -48,7 +48,7 @@ module Inspec
|
|
48
48
|
|
49
49
|
if @qualifier.length > 1
|
50
50
|
last = @qualifier[-1]
|
51
|
-
# preventing its(
|
51
|
+
# preventing its('to_i') as the value returned is always 0
|
52
52
|
if last.length == 1 && last[0] != 'to_i'
|
53
53
|
xres = last[0]
|
54
54
|
else
|
@@ -63,10 +63,10 @@ module Inspec
|
|
63
63
|
vars = variables.map(&:to_ruby).join("\n")
|
64
64
|
vars += "\n" unless vars.empty?
|
65
65
|
res, xtra = describe_chain
|
66
|
-
itsy = xtra.nil? ? 'it' : 'its(' + xtra.
|
66
|
+
itsy = xtra.nil? ? 'it' : 'its(' + xtra.to_s.inspect + ')'
|
67
67
|
naughty = @negated ? '_not' : ''
|
68
|
-
xpect = defined?(@expectation) ? expectation.inspect : ''
|
69
|
-
format("%sdescribe %s do\n %s { should%s %s %s
|
68
|
+
xpect = defined?(@expectation) ? expectation.inspect+' ' : ''
|
69
|
+
format("%sdescribe %s do\n %s { should%s %s %s}\nend",
|
70
70
|
vars, res, itsy, naughty, matcher, xpect)
|
71
71
|
end
|
72
72
|
|
data/lib/inspec/profile.rb
CHANGED
@@ -11,7 +11,6 @@ require 'inspec/metadata'
|
|
11
11
|
module Inspec
|
12
12
|
class Profile # rubocop:disable Metrics/ClassLength
|
13
13
|
extend Forwardable
|
14
|
-
attr_reader :path
|
15
14
|
|
16
15
|
def self.resolve_target(target, opts)
|
17
16
|
# Fetchers retrieve file contents
|
@@ -35,6 +34,7 @@ module Inspec
|
|
35
34
|
end
|
36
35
|
|
37
36
|
attr_reader :source_reader
|
37
|
+
attr_accessor :runner_context
|
38
38
|
def_delegator :@source_reader, :tests
|
39
39
|
def_delegator :@source_reader, :libraries
|
40
40
|
def_delegator :@source_reader, :metadata
|
@@ -46,6 +46,7 @@ module Inspec
|
|
46
46
|
@logger = @options[:logger] || Logger.new(nil)
|
47
47
|
@source_reader = source_reader
|
48
48
|
@profile_id = @options[:id]
|
49
|
+
@runner_context = nil
|
49
50
|
Metadata.finalize(@source_reader.metadata, @profile_id)
|
50
51
|
end
|
51
52
|
|
@@ -55,24 +56,16 @@ module Inspec
|
|
55
56
|
|
56
57
|
def info
|
57
58
|
res = params.dup
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
data[:impact] ||= 0.5
|
67
|
-
data[:impact] = 1.0 if data[:impact] > 1.0
|
68
|
-
data[:impact] = 0.0 if data[:impact] < 0.0
|
69
|
-
rules[gid][:rules][id] = data
|
70
|
-
# TODO: temporarily flatten the group down; replace this with
|
71
|
-
# proper hierarchy later on
|
72
|
-
rules[gid][:title] = data[:group_title]
|
73
|
-
end
|
59
|
+
controls = res[:controls].map do |id, rule|
|
60
|
+
next if id.to_s.empty?
|
61
|
+
data = rule.dup
|
62
|
+
data.delete(:checks)
|
63
|
+
data[:impact] ||= 0.5
|
64
|
+
data[:impact] = 1.0 if data[:impact] > 1.0
|
65
|
+
data[:impact] = 0.0 if data[:impact] < 0.0
|
66
|
+
[id, data]
|
74
67
|
end
|
75
|
-
res[:
|
68
|
+
res[:controls] = Hash[controls.compact]
|
76
69
|
res
|
77
70
|
end
|
78
71
|
|
@@ -137,7 +130,7 @@ module Inspec
|
|
137
130
|
warn.call(@target, 0, 0, nil, 'Profile uses deprecated `test` directory, rename it to `controls`.')
|
138
131
|
end
|
139
132
|
|
140
|
-
count =
|
133
|
+
count = controls_count
|
141
134
|
result[:summary][:controls] = count
|
142
135
|
if count == 0
|
143
136
|
warn.call(nil, nil, nil, nil, 'No controls or tests were defined.')
|
@@ -146,18 +139,15 @@ module Inspec
|
|
146
139
|
end
|
147
140
|
|
148
141
|
# iterate over hash of groups
|
149
|
-
params[:
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
warn.call(sfile, sline, nil, id, "Control #{id} has impact < 0.0") if control[:impact].to_f < 0.0
|
159
|
-
warn.call(sfile, sline, nil, id, "Control #{id} has no tests defined") if control[:checks].nil? or control[:checks].empty?
|
160
|
-
}
|
142
|
+
params[:controls].each { |id, control|
|
143
|
+
sfile, sline = control[:source_location]
|
144
|
+
error.call(sfile, sline, nil, id, 'Avoid controls with empty IDs') if id.nil? or id.empty?
|
145
|
+
next if id.start_with? '(generated '
|
146
|
+
warn.call(sfile, sline, nil, id, "Control #{id} has no title") if control[:title].to_s.empty?
|
147
|
+
warn.call(sfile, sline, nil, id, "Control #{id} has no description") if control[:desc].to_s.empty?
|
148
|
+
warn.call(sfile, sline, nil, id, "Control #{id} has impact > 1.0") if control[:impact].to_f > 1.0
|
149
|
+
warn.call(sfile, sline, nil, id, "Control #{id} has impact < 0.0") if control[:impact].to_f < 0.0
|
150
|
+
warn.call(sfile, sline, nil, id, "Control #{id} has no tests defined") if control[:checks].nil? or control[:checks].empty?
|
161
151
|
}
|
162
152
|
|
163
153
|
# profile is valid if we could not find any error
|
@@ -167,8 +157,8 @@ module Inspec
|
|
167
157
|
result
|
168
158
|
end
|
169
159
|
|
170
|
-
def
|
171
|
-
params[:
|
160
|
+
def controls_count
|
161
|
+
params[:controls].values.length
|
172
162
|
end
|
173
163
|
|
174
164
|
# generates a archive of a folder profile
|
@@ -233,38 +223,63 @@ module Inspec
|
|
233
223
|
def load_params
|
234
224
|
params = @source_reader.metadata.params
|
235
225
|
params[:name] = @profile_id unless @profile_id.nil?
|
236
|
-
params
|
226
|
+
load_checks_params(params)
|
227
|
+
@profile_id ||= params[:name]
|
228
|
+
params
|
229
|
+
end
|
230
|
+
|
231
|
+
def load_checks_params(params)
|
232
|
+
params[:controls] = controls = {}
|
233
|
+
params[:groups] = groups = {}
|
237
234
|
prefix = @source_reader.target.prefix || ''
|
238
235
|
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
checks: Inspec::Rule.checks(rule),
|
260
|
-
code: rule.instance_variable_get(:@__code),
|
261
|
-
source_location: rule.instance_variable_get(:@__source_location),
|
262
|
-
group_title: rule.instance_variable_get(:@__group_title),
|
263
|
-
}
|
236
|
+
if @runner_context.nil?
|
237
|
+
# we're checking a profile, we don't care if it runs on the host machine
|
238
|
+
opts = @options.dup
|
239
|
+
opts[:ignore_supports] = true
|
240
|
+
runner = Runner.new(
|
241
|
+
id: @profile_id,
|
242
|
+
backend: :mock,
|
243
|
+
test_collector: opts.delete(:test_collector),
|
244
|
+
)
|
245
|
+
runner.add_profile(self, opts)
|
246
|
+
runner.rules.values.each do |rule|
|
247
|
+
f = load_rule_filepath(prefix, rule)
|
248
|
+
load_rule(rule, f, controls, groups)
|
249
|
+
end
|
250
|
+
else
|
251
|
+
# load from context
|
252
|
+
@runner_context.rules.values.each do |rule|
|
253
|
+
f = load_rule_filepath(prefix, rule)
|
254
|
+
load_rule(rule, f, controls, groups)
|
255
|
+
end
|
264
256
|
end
|
257
|
+
end
|
265
258
|
|
266
|
-
|
267
|
-
|
259
|
+
def load_rule_filepath(prefix, rule)
|
260
|
+
file = rule.instance_variable_get(:@__file)
|
261
|
+
file = file[prefix.length..-1] if file.start_with?(prefix)
|
262
|
+
file
|
263
|
+
end
|
264
|
+
|
265
|
+
def load_rule(rule, file, controls, groups)
|
266
|
+
id = Inspec::Rule.rule_id(rule)
|
267
|
+
controls[id] = {
|
268
|
+
title: rule.title,
|
269
|
+
desc: rule.desc,
|
270
|
+
impact: rule.impact,
|
271
|
+
refs: rule.ref,
|
272
|
+
tags: rule.tag,
|
273
|
+
checks: Inspec::Rule.checks(rule),
|
274
|
+
code: rule.instance_variable_get(:@__code),
|
275
|
+
source_location: rule.instance_variable_get(:@__source_location),
|
276
|
+
}
|
277
|
+
|
278
|
+
groups[file] ||= {
|
279
|
+
title: rule.instance_variable_get(:@__group_title),
|
280
|
+
controls: [],
|
281
|
+
}
|
282
|
+
groups[file][:controls].push(id)
|
268
283
|
end
|
269
284
|
end
|
270
285
|
end
|
@@ -41,24 +41,19 @@ module Inspec
|
|
41
41
|
end
|
42
42
|
|
43
43
|
def unregister_rule(id)
|
44
|
-
|
45
|
-
@rules[full_id] = nil
|
44
|
+
@rules.delete(full_id(@profile_id, id))
|
46
45
|
end
|
47
46
|
|
48
47
|
def register_rule(r)
|
49
48
|
# get the full ID
|
50
49
|
r.instance_variable_set(:@__file, @current_load[:file])
|
51
50
|
r.instance_variable_set(:@__group_title, @current_load[:title])
|
52
|
-
full_id = Inspec::Rule.full_id(@profile_id, r)
|
53
|
-
if full_id.nil?
|
54
|
-
# TODO: error
|
55
|
-
return
|
56
|
-
end
|
57
51
|
|
58
52
|
# add the rule to the registry
|
59
|
-
|
53
|
+
fid = full_id(Inspec::Rule.profile_id(r), Inspec::Rule.rule_id(r))
|
54
|
+
existing = @rules[fid]
|
60
55
|
if existing.nil?
|
61
|
-
@rules[
|
56
|
+
@rules[fid] = r
|
62
57
|
else
|
63
58
|
Inspec::Rule.merge(existing, r)
|
64
59
|
end
|
@@ -70,6 +65,11 @@ module Inspec
|
|
70
65
|
|
71
66
|
private
|
72
67
|
|
68
|
+
def full_id(pid, rid)
|
69
|
+
return rid.to_s if pid.to_s.empty?
|
70
|
+
pid.to_s + '/' + rid.to_s
|
71
|
+
end
|
72
|
+
|
73
73
|
# Create the context for controls. This includes all components of the DSL,
|
74
74
|
# including matchers and resources.
|
75
75
|
#
|
@@ -93,6 +93,7 @@ module Inspec
|
|
93
93
|
# @return [ProfileContextClass]
|
94
94
|
def create_context(resources_dsl, rule_class) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
95
95
|
profile_context_owner = self
|
96
|
+
profile_id = @profile_id
|
96
97
|
|
97
98
|
# rubocop:disable Lint/NestedMethodDefinition
|
98
99
|
Class.new do
|
@@ -116,7 +117,7 @@ module Inspec
|
|
116
117
|
define_method :control do |*args, &block|
|
117
118
|
id = args[0]
|
118
119
|
opts = args[1] || {}
|
119
|
-
register_control(rule_class.new(id, opts, &block))
|
120
|
+
register_control(rule_class.new(id, profile_id, opts, &block))
|
120
121
|
end
|
121
122
|
|
122
123
|
define_method :describe do |*args, &block|
|
@@ -124,7 +125,7 @@ module Inspec
|
|
124
125
|
id = "(generated from #{loc} #{SecureRandom.hex})"
|
125
126
|
|
126
127
|
res = nil
|
127
|
-
rule = rule_class.new(id, {}) do
|
128
|
+
rule = rule_class.new(id, profile_id, {}) do
|
128
129
|
res = describe(*args, &block)
|
129
130
|
end
|
130
131
|
register_control(rule, &block)
|
@@ -5,43 +5,46 @@
|
|
5
5
|
require 'rspec/core'
|
6
6
|
require 'rspec/core/formatters/json_formatter'
|
7
7
|
|
8
|
-
#
|
9
|
-
#
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
status: example.execution_result.status.to_s,
|
20
|
-
file_path: example.metadata['file_path'],
|
21
|
-
line_number: example.metadata['line_number'],
|
22
|
-
run_time: example.execution_result.run_time,
|
23
|
-
pending_message: example.execution_result.pending_message,
|
24
|
-
id: example.metadata[:id],
|
25
|
-
impact: example.metadata[:impact],
|
26
|
-
}
|
27
|
-
end
|
8
|
+
# Vanilla RSpec JSON formatter with a slight extension to show example IDs.
|
9
|
+
# TODO: Remove these lines when RSpec includes the ID natively
|
10
|
+
class InspecRspecVanilla < RSpec::Core::Formatters::JsonFormatter
|
11
|
+
RSpec::Core::Formatters.register self, :message, :dump_summary, :dump_profile, :stop, :close
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def format_example(example)
|
16
|
+
res = super(example)
|
17
|
+
res[:id] = example.metadata[:id]
|
18
|
+
res
|
28
19
|
end
|
29
20
|
end
|
30
21
|
|
31
|
-
|
22
|
+
# Minimal JSON formatter for inspec. Only contains limited information about
|
23
|
+
# examples without any extras.
|
24
|
+
class InspecRspecMiniJson < RSpec::Core::Formatters::JsonFormatter
|
32
25
|
RSpec::Core::Formatters.register self, :message, :dump_summary, :dump_profile, :stop, :close
|
33
26
|
|
34
|
-
def
|
35
|
-
@
|
36
|
-
@
|
27
|
+
def dump_summary(summary)
|
28
|
+
@output_hash[:version] = Inspec::VERSION
|
29
|
+
@output_hash[:summary] = {
|
30
|
+
duration: summary.duration,
|
31
|
+
example_count: summary.example_count,
|
32
|
+
failure_count: summary.failure_count,
|
33
|
+
skip_count: summary.pending_count,
|
34
|
+
}
|
37
35
|
end
|
38
36
|
|
39
|
-
def
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
37
|
+
def stop(notification)
|
38
|
+
@output_hash[:controls] = notification.examples.map do |example|
|
39
|
+
format_example(example).tap do |hash|
|
40
|
+
e = example.exception
|
41
|
+
next unless e
|
42
|
+
hash[:message] = e.message
|
43
|
+
|
44
|
+
next if e.is_a? RSpec::Expectations::ExpectationNotMetError
|
45
|
+
hash[:exception] = e.class.name
|
46
|
+
hash[:backtrace] = e.backtrace
|
47
|
+
end
|
45
48
|
end
|
46
49
|
end
|
47
50
|
|
@@ -50,21 +53,71 @@ class InspecRspecFormatter < RSpec::Core::Formatters::JsonFormatter
|
|
50
53
|
def format_example(example)
|
51
54
|
res = {
|
52
55
|
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
56
|
status: example.execution_result.status.to_s,
|
58
57
|
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
58
|
}
|
64
59
|
|
65
|
-
|
66
|
-
|
60
|
+
unless (pid = example.metadata[:profile_id]).nil?
|
61
|
+
res[:profile_id] = pid
|
62
|
+
end
|
63
|
+
|
64
|
+
if res[:status] == 'pending'
|
65
|
+
res[:status] = 'skipped'
|
66
|
+
res[:skip_message] = example.metadata[:description]
|
67
|
+
res[:resource] = example.metadata[:described_class].to_s
|
68
|
+
end
|
67
69
|
|
68
70
|
res
|
69
71
|
end
|
70
72
|
end
|
73
|
+
|
74
|
+
class InspecRspecJson < InspecRspecMiniJson
|
75
|
+
RSpec::Core::Formatters.register self, :message, :dump_summary, :dump_profile, :stop, :close
|
76
|
+
|
77
|
+
def add_profile(profile)
|
78
|
+
@profiles ||= []
|
79
|
+
@profiles.push(profile)
|
80
|
+
end
|
81
|
+
|
82
|
+
def dump_one_example(example, profiles, missing)
|
83
|
+
profile = profiles[example[:profile_id]]
|
84
|
+
return missing.push(example) if profile.nil? || profile[:controls].nil?
|
85
|
+
|
86
|
+
control = profile[:controls][example[:id]]
|
87
|
+
return missing.push(example) if control.nil?
|
88
|
+
|
89
|
+
control[:results] ||= []
|
90
|
+
example.delete(:id)
|
91
|
+
example.delete(:profile_id)
|
92
|
+
control[:results].push(example)
|
93
|
+
end
|
94
|
+
|
95
|
+
def profile_info(profile)
|
96
|
+
info = profile.info.dup
|
97
|
+
[info[:name], info]
|
98
|
+
end
|
99
|
+
|
100
|
+
def dump_summary(summary)
|
101
|
+
super(summary)
|
102
|
+
@profiles ||= []
|
103
|
+
examples = @output_hash.delete(:controls)
|
104
|
+
profiles = Hash[@profiles.map { |x| profile_info(x) }]
|
105
|
+
missing = []
|
106
|
+
|
107
|
+
examples.each do |example|
|
108
|
+
dump_one_example(example, profiles, missing)
|
109
|
+
end
|
110
|
+
|
111
|
+
@output_hash[:profiles] = profiles
|
112
|
+
@output_hash[:other_checks] = missing
|
113
|
+
end
|
114
|
+
|
115
|
+
private
|
116
|
+
|
117
|
+
def format_example(example)
|
118
|
+
super(example).tap do |res|
|
119
|
+
res[:run_time] = example.execution_result.run_time
|
120
|
+
res[:start_time] = example.execution_result.started_at.to_s
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|