abide_dev_utils 0.4.2 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -1
  3. data/.rubocop.yml +1 -1
  4. data/CODEOWNERS +1 -0
  5. data/README.md +34 -0
  6. data/abide_dev_utils.gemspec +11 -7
  7. data/itests.rb +138 -0
  8. data/lib/abide_dev_utils/cli/abstract.rb +2 -0
  9. data/lib/abide_dev_utils/cli/comply.rb +115 -0
  10. data/lib/abide_dev_utils/cli/jira.rb +2 -2
  11. data/lib/abide_dev_utils/cli/puppet.rb +136 -11
  12. data/lib/abide_dev_utils/cli/xccdf.rb +26 -7
  13. data/lib/abide_dev_utils/cli.rb +2 -0
  14. data/lib/abide_dev_utils/comply.rb +498 -0
  15. data/lib/abide_dev_utils/config.rb +19 -0
  16. data/lib/abide_dev_utils/errors/comply.rb +17 -0
  17. data/lib/abide_dev_utils/errors/gcloud.rb +27 -0
  18. data/lib/abide_dev_utils/errors/general.rb +5 -0
  19. data/lib/abide_dev_utils/errors/ppt.rb +12 -0
  20. data/lib/abide_dev_utils/errors/xccdf.rb +8 -0
  21. data/lib/abide_dev_utils/errors.rb +2 -0
  22. data/lib/abide_dev_utils/gcloud.rb +22 -0
  23. data/lib/abide_dev_utils/jira.rb +15 -0
  24. data/lib/abide_dev_utils/mixins.rb +16 -0
  25. data/lib/abide_dev_utils/output.rb +7 -3
  26. data/lib/abide_dev_utils/ppt/api.rb +219 -0
  27. data/lib/abide_dev_utils/ppt/class_utils.rb +184 -0
  28. data/lib/abide_dev_utils/ppt/coverage.rb +2 -3
  29. data/lib/abide_dev_utils/ppt/score_module.rb +162 -0
  30. data/lib/abide_dev_utils/ppt.rb +138 -49
  31. data/lib/abide_dev_utils/validate.rb +5 -1
  32. data/lib/abide_dev_utils/version.rb +1 -1
  33. data/lib/abide_dev_utils/xccdf.rb +567 -9
  34. data/lib/abide_dev_utils.rb +1 -0
  35. metadata +82 -17
  36. data/lib/abide_dev_utils/utils/general.rb +0 -9
  37. data/lib/abide_dev_utils/xccdf/cis/hiera.rb +0 -161
  38. data/lib/abide_dev_utils/xccdf/cis.rb +0 -3
@@ -1,24 +1,582 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'abide_dev_utils/output'
3
+ require 'yaml'
4
+ require 'hashdiff'
5
+ require 'nokogiri'
4
6
  require 'abide_dev_utils/validate'
5
- require 'abide_dev_utils/xccdf/cis'
7
+ require 'abide_dev_utils/errors/xccdf'
8
+ require 'abide_dev_utils/output'
6
9
 
7
10
  module AbideDevUtils
11
+ # Contains modules and classes for working with XCCDF files
8
12
  module XCCDF
9
- def self.parse(xccdf_file)
10
- AbideDevUtils::Validate.file(xccdf_file)
11
- Nokogiri.XML(File.open(xccdf_file))
12
- end
13
-
14
- def self.to_hiera(xccdf_file, opts = {})
13
+ # Converts and xccdf file to a Hiera representation
14
+ def self.to_hiera(xccdf_file, opts)
15
15
  type = opts.fetch(:type, 'cis')
16
16
  case type.downcase
17
17
  when 'cis'
18
- AbideDevUtils::XCCDF::CIS::Hiera.new(xccdf_file, parent_key_prefix: opts[:parent_key_prefix], num: opts[:num])
18
+ Benchmark.new(xccdf_file).to_hiera(**opts)
19
19
  else
20
20
  AbideDevUtils::Output.simple("XCCDF type #{type} is unsupported!")
21
21
  end
22
22
  end
23
+
24
+ # Diffs two xccdf files
25
+ def self.diff(file1, file2, opts)
26
+ bm1 = Benchmark.new(file1)
27
+ bm2 = Benchmark.new(file2)
28
+ profile = opts.fetch(:profile, nil)
29
+ profile_diff = if profile.nil?
30
+ bm1.diff_profiles(bm2).each do |_, v|
31
+ v.transform_values! { |x| x.map!(&:to_s) }
32
+ end
33
+ else
34
+ bm1.diff_profiles(bm2)[profile].transform_values! { |x| x.map!(&:to_s) }
35
+ end
36
+ profile_key = profile.nil? ? 'all_profiles' : profile
37
+ {
38
+ 'benchmark' => bm1.diff_title_version(bm2),
39
+ profile_key => profile_diff
40
+ }
41
+ end
42
+
43
+ # Common constants and methods included by nearly everything else
44
+ module Common
45
+ XPATHS = {
46
+ benchmark: {
47
+ all: 'xccdf:Benchmark',
48
+ title: 'xccdf:Benchmark/xccdf:title',
49
+ version: 'xccdf:Benchmark/xccdf:version'
50
+ },
51
+ cis: {
52
+ profiles: {
53
+ all: 'xccdf:Benchmark/xccdf:Profile',
54
+ relative_title: './xccdf:title',
55
+ relative_select: './xccdf:select'
56
+ }
57
+ }
58
+ }.freeze
59
+ CONTROL_PREFIX = /^[\d.]+_/.freeze
60
+ UNDERSCORED = /(\s|\(|\)|-|\.)/.freeze
61
+ CIS_NEXT_GEN_WINDOWS = /[Nn]ext_[Gg]eneration_[Ww]indows_[Ss]ecurity/.freeze
62
+ CIS_CONTROL_NUMBER = /([0-9.]+[0-9]+)/.freeze
63
+ CIS_LEVEL_CODE = /(?:_|^)([Ll]evel_[0-9]|[Ll]1|[Ll]2|[NnBb][GgLl]|#{CIS_NEXT_GEN_WINDOWS})/.freeze
64
+ CIS_CONTROL_PARTS = /#{CIS_CONTROL_NUMBER}#{CIS_LEVEL_CODE}?_+([A-Za-z].*)/.freeze
65
+ CIS_PROFILE_PARTS = /#{CIS_LEVEL_CODE}[_-]+([A-Za-z].*)/.freeze
66
+
67
+ def xpath(path)
68
+ @xml.xpath(path)
69
+ end
70
+
71
+ def validate_xccdf(path)
72
+ AbideDevUtils::Validate.file(path, extension: '.xml')
73
+ end
74
+
75
+ def normalize_string(str)
76
+ nstr = str.dup.downcase
77
+ nstr.gsub!(/[^a-z0-9]$/, '')
78
+ nstr.gsub!(/^[^a-z]/, '')
79
+ nstr.gsub!(/(?:_|^)([Ll]1_|[Ll]2_|ng_)/, '')
80
+ nstr.delete!('(/|\\|\+)')
81
+ nstr.gsub!(UNDERSCORED, '_')
82
+ nstr.strip!
83
+ nstr
84
+ end
85
+
86
+ def normalize_profile_name(prof, **_opts)
87
+ prof_name = normalize_string("profile_#{control_profile_text(prof)}").dup
88
+ prof_name.gsub!(CIS_NEXT_GEN_WINDOWS, 'ngws')
89
+ prof_name.delete_suffix!('_environment_general_use')
90
+ prof_name.delete_suffix!('sensitive_data_environment_limited_functionality')
91
+ prof_name.strip!
92
+ prof_name
93
+ end
94
+
95
+ def normalize_control_name(control, number_format: false)
96
+ return number_normalize_control(control) if number_format
97
+
98
+ name_normalize_control(control)
99
+ end
100
+
101
+ def name_normalize_control(control)
102
+ normalize_string(control_profile_text(control).gsub(CONTROL_PREFIX, ''))
103
+ end
104
+
105
+ def number_normalize_control(control)
106
+ numpart = CONTROL_PREFIX.match(control_profile_text(control)).to_s.chop.gsub(UNDERSCORED, '_')
107
+ "c#{numpart}"
108
+ end
109
+
110
+ def text_normalize(control)
111
+ control_profile_text(control).tr('_', ' ')
112
+ end
113
+
114
+ def profile_parts(profile)
115
+ parts = control_profile_text(profile).match(CIS_PROFILE_PARTS)
116
+ raise AbideDevUtils::Errors::ProfilePartsError, profile if parts.nil?
117
+
118
+ parts[1].gsub!(/[Ll]evel_/, 'L')
119
+ parts[1..2]
120
+ end
121
+
122
+ def control_parts(control, parent_level: nil)
123
+ mdata = control_profile_text(control).match(CIS_CONTROL_PARTS)
124
+ raise AbideDevUtils::Errors::ControlPartsError, control if mdata.nil?
125
+
126
+ mdata[2] = parent_level unless parent_level.nil?
127
+ mdata[1..3]
128
+ end
129
+
130
+ def control_profile_text(item)
131
+ return item.raw_title if item.respond_to?(:abide_object?)
132
+
133
+ if item.respond_to?(:split)
134
+ return item.split('benchmarks_rule_')[-1] if item.include?('benchmarks_rule_')
135
+
136
+ item.split('benchmarks_profile_')[-1]
137
+ else
138
+ return item['idref'].to_s.split('benchmarks_rule_')[-1] if item.name == 'select'
139
+
140
+ item['id'].to_s.split('benchmarks_profile_')[-1]
141
+ end
142
+ end
143
+
144
+ def sorted_control_classes(raw_select_list, sort_key: :number)
145
+ raw_select_list.map { |x| Control.new(x) }.sort_by(&sort_key)
146
+ end
147
+
148
+ def sorted_profile_classes(raw_profile_list, sort_key: :title)
149
+ raw_profile_list.map { |x| Profile.new(x) }.sort_by(&sort_key)
150
+ end
151
+
152
+ def ==(other)
153
+ diff_properties.map { |x| send(x) } == other.diff_properties.map { |x| other.send(x) }
154
+ end
155
+
156
+ def default_diff_opts
157
+ {
158
+ similarity: 1,
159
+ strict: true,
160
+ strip: true,
161
+ array_path: true,
162
+ delimiter: '//',
163
+ use_lcs: false
164
+ }
165
+ end
166
+
167
+ def diff(other, **opts)
168
+ Hashdiff.diff(
169
+ to_h,
170
+ other.to_h,
171
+ default_diff_opts.merge(opts)
172
+ )
173
+ end
174
+
175
+ def abide_object?
176
+ true
177
+ end
178
+ end
179
+
180
+ # Class representation of an XCCDF benchmark
181
+ class Benchmark
182
+ include AbideDevUtils::XCCDF::Common
183
+
184
+ attr_reader :xml, :title, :version, :diff_properties
185
+
186
+ def initialize(path)
187
+ @xml = parse(path)
188
+ @title = xpath('xccdf:Benchmark/xccdf:title').text
189
+ @version = xpath('xccdf:Benchmark/xccdf:version').text
190
+ @diff_properties = %i[title version profiles]
191
+ end
192
+
193
+ def normalized_title
194
+ normalize_string(title)
195
+ end
196
+
197
+ def profiles
198
+ @profiles ||= Profiles.new(xpath('xccdf:Benchmark/xccdf:Profile'))
199
+ end
200
+
201
+ def profile_levels
202
+ @profiles.levels
203
+ end
204
+
205
+ def profile_titles
206
+ @profiles.titles
207
+ end
208
+
209
+ def controls
210
+ @controls ||= Controls.new(xpath('//xccdf:select'))
211
+ end
212
+
213
+ def controls_by_profile_level(level_code)
214
+ profiles.select { |x| x.level == level_code }.map(&:controls).flatten.uniq
215
+ end
216
+
217
+ def controls_by_profile_title(profile_title)
218
+ profiles.select { |x| x.title == profile_title }.controls
219
+ end
220
+
221
+ def find_cis_recommendation(name, number_format: false)
222
+ profiles.each do |profile|
223
+ profile.controls.each do |ctrl|
224
+ return [profile, ctrl] if normalize_control_name(ctrl, number_format: number_format) == name
225
+ end
226
+ end
227
+ end
228
+
229
+ def to_h
230
+ {
231
+ title: title,
232
+ version: version,
233
+ profiles: profiles.to_h
234
+ }
235
+ end
236
+
237
+ def diff_title_version(other)
238
+ Hashdiff.diff(
239
+ to_h.reject { |k, _| k.to_s == 'profiles' },
240
+ other.to_h.reject { |k, _| k.to_s == 'profiles' },
241
+ default_diff_opts
242
+ )
243
+ end
244
+
245
+ def diff_profiles(other)
246
+ this_diff = {}
247
+ other_hash = other.to_h[:profiles]
248
+ to_h[:profiles].each do |name, data|
249
+ diff_h = Hashdiff.diff(data, other_hash[name], default_diff_opts).each_with_object({}) do |x, a|
250
+ val_to = x.length == 4 ? x[3] : nil
251
+ a_key = x[2].is_a?(Hash) ? x[2][:title] : x[2]
252
+ a[a_key] = [] unless a.key?(a_key)
253
+ a[a_key] << ChangeSet.new(change: x[0], key: x[1], value: x[2], value_to: val_to)
254
+ end
255
+ this_diff[name] = diff_h
256
+ end
257
+ this_diff
258
+ end
259
+
260
+ def diff_controls(other)
261
+ controls.diff(other.controls)
262
+ end
263
+
264
+ # Converts object to Hiera-formatted YAML
265
+ # @return [String] YAML-formatted string
266
+ def to_hiera(parent_key_prefix: nil, num: false, levels: [], titles: [], **_kwargs)
267
+ hash = { 'title' => title, 'version' => version }
268
+ key_prefix = hiera_parent_key(parent_key_prefix)
269
+ profiles.each do |profile|
270
+ next unless levels.empty? || levels.include?(profile.level)
271
+ next unless titles.empty? || titles.include?(profile.title)
272
+
273
+ hash[profile.hiera_title] = hiera_controls_for_profile(profile, num)
274
+ end
275
+ hash.transform_keys! do |k|
276
+ [key_prefix, k].join('::').strip
277
+ end
278
+ hash.to_yaml
279
+ end
280
+
281
+ def resolve_control_reference(control)
282
+ xpath("//xccdf:Rule[@id='#{control.reference}']")
283
+ end
284
+
285
+ private
286
+
287
+ def parse(path)
288
+ validate_xccdf(path)
289
+ Nokogiri::XML.parse(File.open(File.expand_path(path))) do |config|
290
+ config.strict.noblanks.norecover
291
+ end
292
+ end
293
+
294
+ def sorted_profile_classes(raw_profile_list, sort_key: :level)
295
+ raw_profile_list.map { |x| Profile.new(x) }.sort_by(&sort_key)
296
+ end
297
+
298
+ def find_profiles
299
+ profs = {}
300
+ xpath('xccdf:Benchmark/xccdf:Profile').each do |profile|
301
+ level_code, name = profile_parts(profile['id'])
302
+ profs[name] = {} unless profs.key?(name)
303
+ profs[name][level_code] = profile
304
+ end
305
+ profs
306
+ end
307
+
308
+ def find_profile_names
309
+ profiles.each_with_object([]) do |profile, ary|
310
+ ary << "#{profile.level} #{profile.plain_text_title}"
311
+ end
312
+ end
313
+
314
+ def hiera_controls_for_profile(profile, number_format)
315
+ profile.controls.each_with_object([]) do |ctrl, ary|
316
+ ary << ctrl.hiera_title(number_format: number_format)
317
+ end
318
+ end
319
+
320
+ def hiera_parent_key(prefix)
321
+ return normalized_title if prefix.nil?
322
+
323
+ prefix.end_with?('::') ? "#{prefix}#{normalized_title}" : "#{prefix}::#{normalized_title}"
324
+ end
325
+ end
326
+
327
+ class ChangeSet
328
+ attr_reader :change, :key, :value, :value_to
329
+
330
+ def initialize(change:, key:, value:, value_to: nil)
331
+ validate_change(change)
332
+ @change = change
333
+ @key = key
334
+ @value = value
335
+ @value_to = value_to
336
+ end
337
+
338
+ def to_s
339
+ val_to_str = value_to.nil? ? ' ' : " to #{value_to} "
340
+ "#{change_string} value #{value}#{val_to_str}at #{key}"
341
+ end
342
+
343
+ def can_merge?(other)
344
+ return false unless (change == '-' && other.change == '+') || (change == '+' && other.change == '-')
345
+ return false unless key == other.key || value_hash_equality(other)
346
+
347
+ true
348
+ end
349
+
350
+ def merge(other)
351
+ unless can_merge?(other)
352
+ raise ArgumentError, 'Cannot merge. Possible causes: change is identical; key or value do not match'
353
+ end
354
+
355
+ new_to_value = value == other.value ? nil : other.value
356
+ ChangeSet.new(
357
+ change: '~',
358
+ key: key,
359
+ value: value,
360
+ value_to: new_to_value
361
+ )
362
+ end
363
+
364
+ private
365
+
366
+ def value_hash_equality(other)
367
+ equality = false
368
+ value.each do |k, v|
369
+ equality = true if v == other.value[k]
370
+ end
371
+ equality
372
+ end
373
+
374
+ def validate_change(change)
375
+ raise ArgumentError, "Change type #{change} in invalid" unless ['+', '-', '~'].include?(change)
376
+ end
377
+
378
+ def change_string
379
+ case change
380
+ when '-'
381
+ 'remove'
382
+ when '+'
383
+ 'add'
384
+ else
385
+ 'change'
386
+ end
387
+ end
388
+ end
389
+
390
+ class ObjectContainer
391
+ include AbideDevUtils::XCCDF::Common
392
+
393
+ def initialize(list, object_creation_method, *args, **kwargs)
394
+ @object_list = send(object_creation_method.to_sym, list, *args, **kwargs)
395
+ @searchable = []
396
+ end
397
+
398
+ def method_missing(m, *args, &block)
399
+ property = m.to_s.start_with?('search_') ? m.to_s.split('_')[-1].to_sym : nil
400
+ return search(property, *args, &block) if property && @searchable.include?(property)
401
+ return @object_list.send(m, *args, &block) if @object_list.respond_to?(m)
402
+
403
+ super
404
+ end
405
+
406
+ def respond_to_missing?(m, include_private = false)
407
+ return true if m.to_s.start_with?('search_') && @searchable.include?(m.to_s.split('_')[-1].to_sym)
408
+
409
+ super
410
+ end
411
+
412
+ def to_h
413
+ @object_list.each_with_object({}) do |obj, self_hash|
414
+ key = resolve_hash_key(obj)
415
+ self_hash[key] = obj.to_h
416
+ end
417
+ end
418
+
419
+ def search(property, item)
420
+ max = @object_list.length - 1
421
+ min = 0
422
+ while min <= max
423
+ mid = (min + max) / 2
424
+ return @object_list[mid] if @object_list[mid].send(property.to_sym) == item
425
+
426
+ if @object_list[mid].send(property.to_sym) > item
427
+ max = mid - 1
428
+ else
429
+ min = mid + 1
430
+ end
431
+ end
432
+ nil
433
+ end
434
+
435
+ private
436
+
437
+ def resolve_hash_key(obj)
438
+ return obj.send(:raw_title) unless defined?(@hash_key)
439
+
440
+ @hash_key.each_with_object([]) { |x, a| a << obj.send(x) }.join('_')
441
+ end
442
+
443
+ def searchable!(*properties)
444
+ @searchable = properties
445
+ end
446
+
447
+ def index!(property)
448
+ @index = property
449
+ end
450
+
451
+ def hash_key!(*properties)
452
+ @hash_key = properties
453
+ end
454
+ end
455
+
456
+ class Profiles < ObjectContainer
457
+ def initialize(list)
458
+ super(list, :sorted_profile_classes)
459
+ searchable! :level, :title
460
+ index! :title
461
+ hash_key! :level, :title
462
+ end
463
+
464
+ def levels
465
+ @levels ||= @object_list.map(&:level).sort
466
+ end
467
+
468
+ def titles
469
+ @titles ||= @object_list.map(&:title).sort
470
+ end
471
+
472
+ def include_level?(item)
473
+ levels.include?(item)
474
+ end
475
+
476
+ def include_title?(item)
477
+ titles.include?(item)
478
+ end
479
+ end
480
+
481
+ class Controls < ObjectContainer
482
+ def initialize(list)
483
+ super(list, :sorted_control_classes)
484
+ searchable! :level, :title, :number
485
+ index! :number
486
+ hash_key! :number
487
+ end
488
+
489
+ def numbers
490
+ @numbers ||= @object_list.map(&:number).sort
491
+ end
492
+
493
+ def levels
494
+ @levels ||= @object_list.map(&:level).sort
495
+ end
496
+
497
+ def titles
498
+ @titles ||= @object_list.map(&:title).sort
499
+ end
500
+
501
+ def include_number?(item)
502
+ numbers.include?(item)
503
+ end
504
+
505
+ def include_level?(item)
506
+ levels.include?(item)
507
+ end
508
+
509
+ def include_title?(item)
510
+ titles.include?(item)
511
+ end
512
+ end
513
+
514
+ class XccdfElement
515
+ include AbideDevUtils::XCCDF::Common
516
+
517
+ def initialize(element)
518
+ @xml = element
519
+ @element_type = self.class.name.split('::').last.downcase
520
+ @raw_title = control_profile_text(element)
521
+ end
522
+
523
+ def to_h
524
+ @properties.each_with_object({}) do |pair, hash|
525
+ hash[pair[0]] = if pair[1].nil?
526
+ send(pair[0])
527
+ else
528
+ obj = send(pair[0])
529
+ obj.send(pair[1])
530
+ end
531
+ end
532
+ end
533
+
534
+ def to_s
535
+ @hash.inspect
536
+ end
537
+
538
+ def reference
539
+ @reference ||= @element_type == 'control' ? @xml['idref'] : @xml['id']
540
+ end
541
+
542
+ def hiera_title(**opts)
543
+ send("normalize_#{@element_type}_name".to_sym, @xml, **opts)
544
+ end
545
+
546
+ private
547
+
548
+ attr_reader :xml
549
+
550
+ def properties(*plain_props, **props)
551
+ plain_props.each { |x| props[x] = nil }
552
+ props.transform_keys!(&:to_sym)
553
+ self.class.class_eval do
554
+ attr_reader :raw_title, :diff_properties
555
+
556
+ plain_props.each { |x| attr_reader x.to_sym unless respond_to?(x) }
557
+ props.each_key { |k| attr_reader k.to_sym unless respond_to?(k) }
558
+ end
559
+ @diff_properties = props.keys
560
+ @properties = props
561
+ end
562
+ end
563
+
564
+ class Profile < XccdfElement
565
+ def initialize(profile)
566
+ super(profile)
567
+ @level, @title = profile_parts(control_profile_text(profile))
568
+ @plain_text_title = @xml.xpath('./xccdf:title').text
569
+ @controls = Controls.new(xpath('./xccdf:select'))
570
+ properties :title, :level, :plain_text_title, controls: :to_h
571
+ end
572
+ end
573
+
574
+ class Control < XccdfElement
575
+ def initialize(control, parent_level: nil)
576
+ super(control)
577
+ @number, @level, @title = control_parts(control_profile_text(control), parent_level: parent_level)
578
+ properties :number, :level, :title
579
+ end
580
+ end
23
581
  end
24
582
  end
@@ -5,6 +5,7 @@ require 'abide_dev_utils/xccdf'
5
5
  require 'abide_dev_utils/ppt'
6
6
  require 'abide_dev_utils/jira'
7
7
  require 'abide_dev_utils/config'
8
+ require 'abide_dev_utils/comply'
8
9
 
9
10
  # Root namespace all modules / classes
10
11
  module AbideDevUtils; end