inspec 0.20.1 → 0.21.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 +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
|