simp-beaker-helpers 2.0.4 → 3.0.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.
@@ -1,479 +0,0 @@
1
- # SIMP Beaker helper methods for testing
2
- module Simp::BeakerHelpers
3
- require 'simp/beaker_helpers/constants'
4
-
5
- # Helpers for working with the SCAP Security Guide
6
- class SSG
7
- if ENV['BEAKER_ssg_repo']
8
- GIT_REPO = ENV['BEAKER_ssg_repo']
9
- else
10
- raise('You are offline: Set BEAKER_ssg_repo to point to the git repo that hosts the SSG content') unless ONLINE
11
-
12
- GIT_REPO = 'https://github.com/ComplianceAsCode/content.git'.freeze
13
- end
14
-
15
- # If this is not set, the highest numeric tag will be used
16
- GIT_BRANCH = nil
17
-
18
- if ENV['BEAKER_ssg_branch']
19
- GIT_BRANCH = ENV['BEAKER_ssg_branch']
20
- end
21
-
22
- EL7_PACKAGES = [
23
- 'PyYAML',
24
- 'cmake',
25
- 'git',
26
- 'openscap-python',
27
- 'openscap-scanner',
28
- 'openscap-utils',
29
- 'python-jinja2',
30
- 'python-lxml',
31
- 'python-setuptools',
32
- ].freeze
33
-
34
- EL8_PACKAGES = [
35
- 'cmake',
36
- 'git',
37
- 'make',
38
- 'openscap-python3',
39
- 'openscap-utils',
40
- 'openscap-scanner',
41
- 'python3',
42
- 'python3-jinja2',
43
- 'python3-lxml',
44
- 'python3-pyyaml',
45
- 'python3-setuptools',
46
- 'libarchive',
47
- ].freeze
48
-
49
- EL9_PACKAGES = [
50
- 'cmake',
51
- 'git',
52
- 'make',
53
- 'openscap-python3',
54
- 'openscap-utils',
55
- 'openscap-scanner',
56
- 'python3',
57
- 'python3-jinja2',
58
- 'python3-lxml',
59
- 'python3-pyyaml',
60
- 'python3-setuptools',
61
- 'libarchive',
62
- ].freeze
63
-
64
- OS_INFO = {
65
- 'RedHat' => {
66
- '6' => {
67
- 'required_packages' => EL7_PACKAGES,
68
- 'ssg' => {
69
- 'profile_target' => 'rhel6',
70
- 'build_target' => 'rhel6',
71
- 'datastream' => 'ssg-rhel6-ds.xml'
72
- }
73
- },
74
- '7' => {
75
- 'required_packages' => EL7_PACKAGES,
76
- 'ssg' => {
77
- 'profile_target' => 'rhel7',
78
- 'build_target' => 'rhel7',
79
- 'datastream' => 'ssg-rhel7-ds.xml'
80
- }
81
- },
82
- '8' => {
83
- 'required_packages' => EL8_PACKAGES,
84
- 'ssg' => {
85
- 'profile_target' => 'rhel8',
86
- 'build_target' => 'rhel8',
87
- 'datastream' => 'ssg-rhel8-ds.xml'
88
- }
89
- },
90
- '9' => {
91
- 'required_packages' => EL9_PACKAGES,
92
- 'ssg' => {
93
- 'profile_target' => 'rhel9',
94
- 'build_target' => 'rhel9',
95
- 'datastream' => 'ssg-rhel9-ds.xml'
96
- }
97
- }
98
- },
99
- 'CentOS' => {
100
- '6' => {
101
- 'required_packages' => EL7_PACKAGES,
102
- 'ssg' => {
103
- 'profile_target' => 'rhel6',
104
- 'build_target' => 'centos6',
105
- 'datastream' => 'ssg-centos6-ds.xml'
106
- }
107
- },
108
- '7' => {
109
- 'required_packages' => EL7_PACKAGES,
110
- 'ssg' => {
111
- 'profile_target' => 'centos7',
112
- 'build_target' => 'centos7',
113
- 'datastream' => 'ssg-centos7-ds.xml'
114
- }
115
- },
116
- '8' => {
117
- 'required_packages' => EL8_PACKAGES,
118
- 'ssg' => {
119
- 'profile_target' => 'centos8',
120
- 'build_target' => 'centos8',
121
- 'datastream' => 'ssg-centos8-ds.xml'
122
- }
123
- },
124
- '9' => {
125
- 'required_packages' => EL9_PACKAGES,
126
- 'ssg' => {
127
- 'profile_target' => 'cs9',
128
- 'build_target' => 'cs9',
129
- 'datastream' => 'ssg-cs9-ds.xml'
130
- }
131
- }
132
- },
133
- 'Rocky' => {
134
- '8' => {
135
- 'required_packages' => EL8_PACKAGES,
136
- 'ssg' => {
137
- 'profile_target' => 'centos8',
138
- 'build_target' => 'centos8',
139
- 'datastream' => 'ssg-centos8-ds.xml'
140
- }
141
- },
142
- '9' => {
143
- 'required_packages' => EL9_PACKAGES,
144
- 'ssg' => {
145
- 'profile_target' => 'cs9',
146
- 'build_target' => 'cs9',
147
- 'datastream' => 'ssg-cs9-ds.xml'
148
- }
149
- }
150
- },
151
- 'OracleLinux' => {
152
- '7' => {
153
- 'required_packages' => EL7_PACKAGES,
154
- 'ssg' => {
155
- 'profile_target' => 'ol7',
156
- 'build_target' => 'ol7',
157
- 'datastream' => 'ssg-ol7-ds.xml'
158
- },
159
- },
160
- '8' => {
161
- 'required_packages' => EL8_PACKAGES,
162
- 'ssg' => {
163
- 'profile_target' => 'ol8',
164
- 'build_target' => 'ol8',
165
- 'datastream' => 'ssg-ol8-ds.xml'
166
- }
167
- },
168
- '9' => {
169
- 'required_packages' => EL9_PACKAGES,
170
- 'ssg' => {
171
- 'profile_target' => 'ol9',
172
- 'build_target' => 'ol9',
173
- 'datastream' => 'ssg-ol9-ds.xml'
174
- }
175
- }
176
- }
177
- }.freeze
178
-
179
- attr_accessor :scap_working_dir
180
-
181
- # Create a new SSG helper for the specified host
182
- #
183
- # @param sut
184
- # The SUT against which to run
185
- #
186
- def initialize(sut)
187
- @sut = sut
188
-
189
- @os = pfact_on(@sut, 'os.name')
190
- @os_rel = pfact_on(@sut, 'os.release.major')
191
-
192
- sut.mkdir_p('scap_working_dir')
193
-
194
- @scap_working_dir = on(sut, 'cd scap_working_dir && pwd').stdout.strip
195
-
196
- unless OS_INFO[@os]
197
- raise("Error: The '#{@os}' Operating System is not supported")
198
- end
199
-
200
- OS_INFO[@os][@os_rel]['required_packages'].each do |pkg|
201
- install_latest_package_on(@sut, pkg)
202
- end
203
-
204
- @output_dir = File.absolute_path('sec_results/ssg')
205
-
206
- unless File.directory?(@output_dir)
207
- FileUtils.mkdir_p(@output_dir)
208
- end
209
-
210
- @result_file = "#{@sut.hostname}-ssg-#{Time.now.to_i}"
211
-
212
- get_ssg_datastream
213
- end
214
-
215
- def profile_target
216
- OS_INFO[@os][@os_rel]['ssg']['profile_target']
217
- end
218
-
219
- def get_profiles
220
- cmd = "cd #{@scap_working_dir}; oscap info --profiles"
221
- on(@sut, "#{cmd} #{OS_INFO[@os][@os_rel]['ssg']['datastream']}")
222
- .stdout
223
- .strip
224
- .lines
225
- .map { |x| x.split(':').first }
226
- end
227
-
228
- def remediate(profile)
229
- evaluate(profile, true)
230
- end
231
-
232
- def evaluate(profile, remediate = false)
233
- cmd = "cd #{@scap_working_dir}; oscap xccdf eval"
234
-
235
- if remediate
236
- cmd += ' --remediate'
237
- end
238
-
239
- cmd += %( --profile #{profile} --results #{@result_file}.xml --report #{@result_file}.html #{OS_INFO[@os][@os_rel]['ssg']['datastream']})
240
-
241
- # We accept all exit codes here because there have occasionally been
242
- # failures in the SSG content and we're not testing that.
243
-
244
- on(@sut, cmd, accept_all_exit_codes: true)
245
-
246
- ['xml', 'html'].each do |ext|
247
- path = "#{@scap_working_dir}/#{@result_file}.#{ext}"
248
- scp_from(@sut, path, @output_dir)
249
-
250
- raise("Could not retrieve #{path} from #{@sut}") unless File.exist?(File.join(@output_dir, "#{@result_file}.#{ext}"))
251
- end
252
- end
253
-
254
- # Output the report
255
- #
256
- # @param report
257
- # The results Hash
258
- #
259
- def write_report(report)
260
- File.open(File.join(@output_dir, @result_file) + '.report', 'w') do |fh|
261
- fh.puts(report[:report].uncolor)
262
- end
263
- end
264
-
265
- # Retrieve a subset of test results based on a match to filter
266
- #
267
- # @param filter [String, Array[String]]
268
- # A 'short name' filter that will be matched against the rule ID name
269
- #
270
- # @param exclusions [String, Array[String]]
271
- # A 'short name' filter of items that will be removed from the `filter`
272
- # matches
273
- #
274
- # @return [Hash] A Hash of statistics and a formatted report
275
- #
276
- # FIXME:
277
- # - This is a hack! Should be searching for rules based on a set
278
- # set of STIG ids, but don't see those ids in the oscap results xml.
279
- # Further mapping is required...
280
- # - Create the same report structure as inspec
281
- def process_ssg_results(filter = nil, exclusions = nil)
282
- self.class.process_ssg_results(
283
- File.join(@output_dir, @result_file) + '.xml',
284
- filter,
285
- exclusions,
286
- )
287
- end
288
-
289
- # Process the results of an SSG run
290
- #
291
- # @param result_file [String]
292
- # The oscap result XML file to process
293
- #
294
- # @param filter [String, Array[String]]
295
- # A 'short name' filter that will be matched against the rule ID name
296
- #
297
- # @param exclusions [String, Array[String]]
298
- # A 'short name' filter of items that will be removed from the `filter`
299
- # matches
300
- #
301
- # @return [Hash] A Hash of statistics and a formatted report
302
- #
303
- def self.process_ssg_results(result_file, filter = nil, exclusions = nil)
304
- require 'highline'
305
- require 'nokogiri'
306
-
307
- HighLine.colorize_strings
308
-
309
- raise("Could not find results XML file '#{result_file}'") unless File.exist?(result_file)
310
-
311
- puts "Processing #{result_file}"
312
- doc = Nokogiri::XML(File.open(result_file))
313
-
314
- # because I'm lazy
315
- doc.remove_namespaces!
316
-
317
- if filter
318
- filter = Array(filter)
319
-
320
- xpath_query = [
321
- '//rule-result[(',
322
- ]
323
-
324
- xpath_query << filter.map { |flt|
325
- "contains(@idref,'#{flt}')"
326
- }.join(' or ')
327
-
328
- xpath_query << ')' if filter.size > 1
329
-
330
- exclusions = Array(exclusions)
331
- unless exclusions.empty?
332
- xpath_query << 'and not('
333
-
334
- xpath_query << exclusions.map { |exl|
335
- "contains(@idref,'#{exl}')"
336
- }.join(' or ')
337
-
338
- xpath_query << ')' unless exclusions.empty?
339
- end
340
-
341
- xpath_query << ')]'
342
-
343
- xpath_query = xpath_query.join(' ')
344
-
345
- # XPATH to get the pertinent test results:
346
- # Any node named 'rule-result' for which the attribute 'idref'
347
- # contains any of the `filter` Strings and does not contain any of the
348
- # `exclusions` Strings
349
- result_nodes = doc.xpath(xpath_query)
350
- else
351
- result_nodes = doc.xpath('//rule-result')
352
- end
353
-
354
- stats = {
355
- failed: [],
356
- passed: [],
357
- skipped: [],
358
- filter: filter.nil? ? 'No Filter' : filter,
359
- report: nil,
360
- score: 0
361
- }
362
-
363
- result_nodes.each do |rule_result|
364
- # Results are recorded in a child node named 'result'.
365
- # Within the 'result' node, the actual result string is
366
- # the content of that node's (only) child node.
367
-
368
- result = rule_result.element_children.at('result')
369
- result_id = rule_result.attributes['idref'].value.to_s
370
- result_value = [
371
- 'Title: ' + doc.xpath("//Rule[@id='#{result_id}']/title/text()").first.to_s,
372
- ' ID: ' + result_id,
373
- ]
374
-
375
- if result.child.content == 'fail'
376
- references = {}
377
-
378
- doc.xpath("//Rule[@id='#{result_id}']/reference").each do |ref|
379
- references[ref['href']] ||= []
380
- references[ref['href']] << ref.text
381
- end
382
-
383
- result_value << ' References:'
384
- references.each_pair do |src, items|
385
- result_value << " * #{src}"
386
- result_value << " * #{items.join(', ')}"
387
- end
388
- result_value << ' Description: ' + doc.xpath("//Rule[@id='#{result_id}']/description").text.gsub("\n", "\n ")
389
- end
390
-
391
- result_value = result_value.join("\n")
392
-
393
- if result.child.content == 'fail'
394
- stats[:failed] << result_value.red
395
- elsif result.child.content == 'pass'
396
- stats[:passed] << result_value.green
397
- else
398
- stats[:skipped] << result_value.yellow
399
- end
400
- end
401
-
402
- report = []
403
-
404
- report << '== Skipped =='
405
- report << stats[:skipped].join("\n")
406
-
407
- report << '== Passed =='
408
- report << stats[:passed].join("\n")
409
-
410
- report << '== Failed =='
411
- report << stats[:failed].join("\n")
412
-
413
- report << 'OSCAP Statistics:'
414
-
415
- if filter
416
- report << " * Used Filter: 'idref' ~= '#{stats[:filter]}'"
417
- end
418
-
419
- report << " * Passed: #{stats[:passed].count.to_s.green}"
420
- report << " * Failed: #{stats[:failed].count.to_s.red}"
421
- report << " * Skipped: #{stats[:skipped].count.to_s.yellow}"
422
-
423
- score = 0
424
-
425
- if (stats[:passed].count + stats[:failed].count) > 0
426
- score = ((stats[:passed].count.to_f / (stats[:passed].count + stats[:failed].count)) * 100.0).round(0)
427
- end
428
-
429
- report << "\n Score: #{score}%"
430
-
431
- stats[:score] = score
432
- stats[:report] = report.join("\n")
433
-
434
- stats
435
- end
436
-
437
- private
438
-
439
- def get_ssg_datastream
440
- # Allow users to point at a specific SSG release 'tar.bz2' file
441
- ssg_release = ENV['BEAKER_ssg_release']
442
-
443
- # Grab the latest SSG release in fixtures if it exists
444
- ssg_release ||= Dir.glob('spec/fixtures/ssg_releases/*.bz2').last
445
-
446
- if ssg_release
447
- copy_to(@sut, ssg_release, @scap_working_dir)
448
-
449
- on(@sut, %(mkdir -p scap-content && tar -xj -C scap-content --strip-components 1 -f #{ssg_release} && cp scap-content/*ds.xml #{@scap_working_dir}))
450
- else
451
- on(@sut, %(git clone #{GIT_REPO} scap-content))
452
- if GIT_BRANCH
453
- on(@sut, %(cd scap-content; git checkout #{GIT_BRANCH}))
454
- else
455
- tags = on(@sut, %(cd scap-content; git tag -l)).output
456
- target_tag = tags.lines.map(&:strip)
457
- .select { |x| x.match?(%r{^v(\d+\.)+\d+$}) }
458
- .sort.last
459
-
460
- on(@sut, %(cd scap-content; git checkout #{target_tag}))
461
- end
462
-
463
- # Work around the issue where the profiles now strip out derivative
464
- # content that isn't explicitlly approved for that OS. This means that
465
- # we are unable to test CentOS builds against the STIG, etc...
466
- #
467
- # This isn't 100% correct but it's "good enough" for an automated CI
468
- # environment to tell us if something is critically out of alignment.
469
- safe_sed(
470
- @sut,
471
- 's/ssg.build_derivatives.profile_handling/__simp_dontcare__ = None #ssg.build_derivatives.profile_handling/g',
472
- 'scap-content/build-scripts/enable_derivatives.py',
473
- )
474
-
475
- on(@sut, %(cd scap-content/build; cmake ../; make -j4 #{OS_INFO[@os][@os_rel]['ssg']['build_target']}-content && cp *ds.xml #{@scap_working_dir}))
476
- end
477
- end
478
- end
479
- end
@@ -1,49 +0,0 @@
1
- require 'spec_helper_acceptance'
2
-
3
- test_name 'SSG Functionality Validation'
4
-
5
- describe 'run the SSG against an SCAP profile' do
6
- hosts.each do |host|
7
- context "on #{host}" do
8
- ssg = nil
9
- ssg_report = nil
10
-
11
- before(:all) do
12
- ssg = Simp::BeakerHelpers::SSG.new(host)
13
-
14
- # If we don't do this, the variable gets reset
15
- ssg_report = { data: nil }
16
- end
17
-
18
- it 'runs the SSG' do
19
- profiles = ssg.get_profiles
20
-
21
- profile = profiles.find { |x| x.include?('_stig') } ||
22
- profiles.find { |x| x.include?('_cui') } ||
23
- profiles.find { |x| x.include?('_ospp') } ||
24
- profiles.find { |x| x.include?('_standard') } ||
25
- profiles.last
26
-
27
- expect(profile).not_to be_nil
28
- ssg.evaluate(profile)
29
- end
30
-
31
- it 'has an SSG report' do
32
- # Validate that the filter works
33
- filter = '_rule_audit'
34
- host_exclusions = ['ssh_']
35
-
36
- ssg_report[:data] = ssg.process_ssg_results(filter, host_exclusions)
37
-
38
- expect(ssg_report[:data]).not_to be_nil
39
-
40
- ssg.write_report(ssg_report[:data])
41
- end
42
-
43
- it 'has a report' do
44
- expect(ssg_report[:data][:report]).not_to be_nil
45
- puts ssg_report[:data][:report]
46
- end
47
- end
48
- end
49
- end
@@ -1,2 +0,0 @@
1
- ---
2
- 'default_run': true
@@ -1 +0,0 @@
1
- ../../nodesets