abide_dev_utils 0.9.7 → 0.10.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.
@@ -0,0 +1,741 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest'
4
+ require_relative './objects/digest_object'
5
+ require_relative './objects/numbered_object'
6
+
7
+ module AbideDevUtils
8
+ module XCCDF
9
+ module Parser
10
+ # Holds individual XCCDF objects
11
+ module Objects
12
+ # Base class for XCCDF element objects
13
+ class ElementBase
14
+ include AbideDevUtils::XCCDF::Parser::Objects::DigestObject
15
+ attr_reader :children, :child_labels, :link_labels
16
+
17
+ def initialize(*_args, **_kwargs)
18
+ @children = []
19
+ @links = []
20
+ @link_labels = []
21
+ @child_labels = []
22
+ @label_method_values = {}
23
+ exclude_from_digest(%i[@digest @children @child_labels @label @exclude_from_digest @label_method_values])
24
+ end
25
+
26
+ # For subclasses that are associated with a specific
27
+ # XCCDF element, this method returns the element's
28
+ # xpath. Must be overridden by subclasses that
29
+ # implement this method.
30
+ def self.xpath
31
+ nil
32
+ end
33
+
34
+ # Takes the last segment of the class name, splits on captial letters,
35
+ # and returns a downcased string joined by dashes. This gives us the
36
+ # XCCDF element type. Example: 'AbideDevUtils::XCCDF::Parser::Objects::ComplexCheck'
37
+ # returns 'complex-check'.
38
+ def xccdf_type
39
+ self.class.name.split('::').last.split(/(?=[A-Z])/).reject { |x| x == 'Xccdf' }.join('-').downcase
40
+ end
41
+
42
+ def all_values
43
+ @child_labels.map { |label| send(label.to_sym) }
44
+ @label_method_values
45
+ end
46
+
47
+ # Allows access to child objects by label
48
+ def method_missing(method_name, *args, &block)
49
+ m_name_string = method_name.to_s.downcase
50
+ return @label_method_values[m_name_string] if @label_method_values.key?(m_name_string)
51
+
52
+ label_str = m_name_string.start_with?('linked_') ? m_name_string.split('_')[1..].join('_') : m_name_string
53
+ if m_name_string.start_with?('linked_') && @link_labels.include?(label_str)
54
+ found = @links.select { |link| link.label == label_str }
55
+ @label_method_values["linked_#{label_str}"] = if found.length == 1
56
+ found.first
57
+ else
58
+ found
59
+ end
60
+ @label_method_values["linked_#{label_str}"]
61
+ elsif @child_labels.include?(label_str)
62
+ found = @children.select { |child| child.label == label_str }
63
+ @label_method_values[label_str] = if found.length == 1
64
+ found.first
65
+ else
66
+ found
67
+ end
68
+ @label_method_values[label_str]
69
+ else
70
+ super
71
+ end
72
+ end
73
+
74
+ def respond_to_missing?(method_name, include_private = false)
75
+ m_name_string = method_name.to_s.downcase
76
+ label_str = m_name_string.start_with?('linked_') ? m_name_string.split('_')[1..].join('_') : m_name_string
77
+ (m_name_string.start_with?('linked_') && @link_labels.include?(label_str)) ||
78
+ @child_labels.include?(label_str) ||
79
+ super
80
+ end
81
+
82
+ def label
83
+ return @label if defined?(@label)
84
+
85
+ @label = case self.class.name
86
+ when 'AbideDevUtils::XCCDF::Parser::Objects::AttributeValue'
87
+ @attribute.to_s
88
+ when /AbideDevUtils::XCCDF::Parser::Objects::(ShortText|LongText)/
89
+ 'text'
90
+ else
91
+ self.class.name.split('::').last.split(/(?=[A-Z])/).join('_').downcase
92
+ end
93
+ @label
94
+ end
95
+
96
+ def recursive_select_children(children_to_search = children, &block)
97
+ search_hits = []
98
+ children_to_search.each do |child|
99
+ found = yield child
100
+ if found
101
+ search_hits << child
102
+ elsif child.respond_to?(:children)
103
+ search_hits << recursive_select_children(child.children, &block)
104
+ end
105
+ end
106
+ search_hits.flatten.compact.uniq
107
+ end
108
+
109
+ def recursive_find_child(children_to_search = children, &block)
110
+ rescursive_select_children(children_to_search, &block).first
111
+ end
112
+
113
+ def find_children_that_respond_to(method, recurse: false)
114
+ return recursive_select_children { |child| child.respond_to?(method) } if recurse
115
+
116
+ children.select { |c| c.respond_to?(method.to_sym) }
117
+ end
118
+
119
+ def find_children_by_class(klass, recurse: false)
120
+ return recursive_select_children { |child| child.instance_of?(klass) } if recurse
121
+
122
+ children.select { |child| child.instance_of?(klass) }
123
+ end
124
+
125
+ def find_child_by_class(klass, recurse: false)
126
+ return recursive_find_child { |child| child.is_a?(klass) } if recurse
127
+
128
+ find_children_by_class(klass).first
129
+ end
130
+
131
+ def find_children_by_xpath(xpath, recurse: false)
132
+ return recursive_select_children { |child| child.xpath == xpath } if recurse
133
+
134
+ children.select { |child| child.xpath == xpath }
135
+ end
136
+
137
+ def find_child_by_xpath(xpath, recurse: false)
138
+ return recursive_find_child { |child| child.xpath == xpath } if recurse
139
+
140
+ find_children_by_xpath(xpath).first
141
+ end
142
+
143
+ def find_children_by_attribute(attribute, recurse: false)
144
+ pr = proc do |child|
145
+ next unless child.instance_of?(AbideDevUtils::XCCDF::Parser::Objects::AttributeValue)
146
+
147
+ child.attribute == attribute
148
+ end
149
+ return recursive_select_children(&pr) if recurse
150
+
151
+ children.select(&pr)
152
+ end
153
+
154
+ def find_child_by_attribute(attribute, recurse: false)
155
+ find_children_by_attribute(attribute, recurse: recurse).first
156
+ end
157
+
158
+ def find_children_by_attribute_value(attribute, value, recurse: false)
159
+ pr = proc do |child|
160
+ next unless child.instance_of?(AbideDevUtils::XCCDF::Parser::Objects::AttributeValue)
161
+
162
+ child.attribute == attribute && child.value == value
163
+ end
164
+ return recursive_select_children(&pr) if recurse
165
+
166
+ children.select(&pr)
167
+ end
168
+
169
+ def find_child_by_attribute_value(attribute, value, recurse: false)
170
+ find_children_by_attribute_value(attribute, value, recurse: recurse).first
171
+ end
172
+
173
+ def add_link(object)
174
+ @links << object
175
+ @link_labels << object.label unless @link_labels.include?(object.label)
176
+ end
177
+
178
+ def add_links(objects)
179
+ objects.each { |object| add_object_as_child(object) }
180
+ end
181
+
182
+ private
183
+
184
+ def with_safe_methods(default: nil)
185
+ yield
186
+ rescue NoMethodError
187
+ default
188
+ end
189
+
190
+ def namespace_safe_xpath(element, path)
191
+ element.xpath(path)
192
+ rescue Nokogiri::XML::XPath::SyntaxError
193
+ element.xpath("*[name()='#{path}']")
194
+ end
195
+
196
+ def namespace_safe_at_xpath(element, path)
197
+ element.at_xpath(path)
198
+ rescue Nokogiri::XML::XPath::SyntaxError
199
+ element.at_xpath("*[name()='#{path}']")
200
+ end
201
+
202
+ def add_child(klass, element, *args, **kwargs)
203
+ return if element.nil?
204
+
205
+ real_element = klass.xpath.nil? ? element : namespace_safe_at_xpath(element, klass.xpath)
206
+ return if real_element.nil?
207
+
208
+ obj = new_object(klass, real_element, *args, **kwargs)
209
+ @children << obj
210
+ @child_labels << obj.label unless @child_labels.include?(obj.label)
211
+ rescue StandardError => e
212
+ raise(
213
+ e,
214
+ "Failed to add child #{klass.to_s.split('::').last} to #{self.class.to_s.split('::').last}: #{e.message}",
215
+ e.backtrace,
216
+ )
217
+ end
218
+
219
+ def add_children(klass, element, *args, **kwargs)
220
+ return if element.nil?
221
+
222
+ real_elements = klass.xpath.nil? ? element : namespace_safe_xpath(element, klass.xpath)
223
+ return if real_elements.nil?
224
+
225
+ real_elements.each do |e|
226
+ obj = new_object(klass, e, *args, **kwargs)
227
+ @children << obj
228
+ @child_labels << obj.label unless @child_labels.include?(obj.label)
229
+ end
230
+ rescue StandardError => e
231
+ raise(
232
+ e,
233
+ "Failed to add children #{klass.to_s.split('::').last} to #{self.class.to_s.split('::').last}: #{e.message}",
234
+ e.backtrace,
235
+ )
236
+ end
237
+
238
+ def new_object(klass, element, *args, **kwargs)
239
+ klass.new(element, *args, **kwargs)
240
+ end
241
+ end
242
+
243
+ # Holds text content that does not use multiple lines
244
+ class ShortText < ElementBase
245
+ attr_reader :text
246
+
247
+ def initialize(element)
248
+ super
249
+ text = element.respond_to?(:text) ? element.text : element
250
+ @text = text.to_s
251
+ end
252
+
253
+ def to_s
254
+ @text
255
+ end
256
+ end
257
+
258
+ # Holds text content that consists of multiple lines
259
+ class LongText < ElementBase
260
+ attr_reader :text
261
+
262
+ def initialize(element)
263
+ super
264
+ text = element.respond_to?(:text) ? element.text : element
265
+ @text = text.to_s
266
+ @string_text = text.to_s.tr("\n", ' ').gsub(/\s+/, ' ')
267
+ end
268
+
269
+ def to_s
270
+ @string_text
271
+ end
272
+ end
273
+
274
+ # Represents a value of an element attribute
275
+ class AttributeValue < ElementBase
276
+ attr_reader :attribute, :value
277
+
278
+ def initialize(element, attribute)
279
+ super
280
+ @attribute = attribute
281
+ @value = element[attribute]
282
+ end
283
+
284
+ def to_s
285
+ "#{@attribute}=#{@value}"
286
+ end
287
+ end
288
+
289
+ # Class for an XCCDF element title
290
+ class Title < ElementBase
291
+ def initialize(element)
292
+ super
293
+ add_child(ShortText, element)
294
+ end
295
+
296
+ def self.xpath
297
+ 'xccdf:title'
298
+ end
299
+
300
+ def to_s
301
+ find_child_by_class(ShortText).to_s
302
+ end
303
+ end
304
+
305
+ # Class for an XCCDF element description
306
+ class Description < ElementBase
307
+ def initialize(element)
308
+ super
309
+ add_child(LongText, element)
310
+ end
311
+
312
+ def self.xpath
313
+ 'xccdf:description'
314
+ end
315
+
316
+ def to_s
317
+ find_child_by_class(LongText).to_s
318
+ end
319
+ end
320
+
321
+ # Base class for elements that have the ID attribute
322
+ class ElementWithId < ElementBase
323
+ attr_reader :id
324
+
325
+ def initialize(element)
326
+ super
327
+ add_child(AttributeValue, element, 'id')
328
+ @id = find_child_by_attribute('id').value.to_s
329
+ end
330
+
331
+ def to_s
332
+ @id
333
+ end
334
+ end
335
+
336
+ # Base class for elements that have the idref attribute
337
+ class ElementWithIdref < ElementBase
338
+ attr_reader :idref
339
+
340
+ def initialize(element)
341
+ super
342
+ add_child(AttributeValue, element, 'idref')
343
+ @idref = find_child_by_attribute('idref').value.to_s
344
+ end
345
+
346
+ def to_s
347
+ @idref
348
+ end
349
+ end
350
+
351
+ # Class for an XCCDF select element
352
+ class XccdfSelect < ElementWithIdref
353
+ def initialize(element)
354
+ super
355
+ add_child(AttributeValue, element, 'selected')
356
+ end
357
+
358
+ def self.xpath
359
+ 'xccdf:select'
360
+ end
361
+ end
362
+
363
+ # Class for XCCDF profile
364
+ class Profile < ElementWithId
365
+ def initialize(element)
366
+ super
367
+ add_child(Title, element)
368
+ add_child(Description, element)
369
+ add_children(XccdfSelect, element)
370
+ end
371
+
372
+ def level
373
+ return @level if defined?(@level)
374
+
375
+ level_match = title.to_s.match(/([Ll]evel [0-9]+)/)
376
+ @level = level_match.nil? ? level_match : level_match[1]
377
+ @level
378
+ end
379
+
380
+ def self.xpath
381
+ 'xccdf:Profile'
382
+ end
383
+ end
384
+
385
+ # Class for XCCDF group
386
+ class Group < ElementWithId
387
+ include AbideDevUtils::XCCDF::Parser::Objects::NumberedObject
388
+ attr_reader :number
389
+
390
+ def initialize(element)
391
+ super
392
+ @number = to_s[/group_([0-9]+\.)+[0-9]+|group_([0-9]+)/].gsub(/group_/, '')
393
+ add_child(Title, element)
394
+ add_child(Description, element)
395
+ add_children(Group, element)
396
+ add_children(Rule, element)
397
+ end
398
+
399
+ def self.xpath
400
+ 'xccdf:Group'
401
+ end
402
+ end
403
+
404
+ # Class for XCCDF check-export
405
+ class CheckExport < ElementBase
406
+ def initialize(element)
407
+ super
408
+ add_child(AttributeValue, element, 'export-name')
409
+ add_child(AttributeValue, element, 'value-id')
410
+ end
411
+
412
+ def self.xpath
413
+ 'xccdf:check-export'
414
+ end
415
+
416
+ def to_s
417
+ [find_child_by_attribute('export-name').to_s, find_child_by_attribute('value-id').to_s].join('|')
418
+ end
419
+ end
420
+
421
+ # Class for XCCDF check-content-ref
422
+ class CheckContentRef < ElementBase
423
+ def initialize(element)
424
+ super
425
+ add_child(AttributeValue, element, 'href')
426
+ add_child(AttributeValue, element, 'name')
427
+ end
428
+
429
+ def self.xpath
430
+ 'xccdf:check-content-ref'
431
+ end
432
+
433
+ def to_s
434
+ [find_child_by_attribute('href').to_s, find_child_by_attribute('name').to_s].join('|')
435
+ end
436
+ end
437
+
438
+ # Class for XCCDF check
439
+ class Check < ElementBase
440
+ def initialize(element)
441
+ super
442
+ add_child(AttributeValue, element, 'system')
443
+ add_children(CheckExport, element)
444
+ add_children(CheckContentRef, element)
445
+ end
446
+
447
+ def self.xpath
448
+ 'xccdf:check'
449
+ end
450
+ end
451
+
452
+ # Class for XCCDF Ident ControlURI element
453
+ class ControlURI < ElementBase
454
+ def initialize(element)
455
+ super
456
+ @namespace = element.attributes['controlURI'].namespace.prefix
457
+ @value = element.attributes['controlURI'].value
458
+ end
459
+
460
+ def to_s
461
+ [label, @namespace, @value].join(':')
462
+ end
463
+ end
464
+
465
+ # Class for XCCDF Ident System element
466
+ class System < ElementBase
467
+ def initialize(element)
468
+ super
469
+ @system = element.attributes['system'].value
470
+ @text = element.text
471
+ end
472
+
473
+ def to_s
474
+ [label, @system, @text].join(':')
475
+ end
476
+ end
477
+
478
+ # Class for XCCDF rule ident
479
+ class Ident < ElementBase
480
+ def initialize(element)
481
+ super
482
+ with_safe_methods { add_child(ControlURI, element) }
483
+ with_safe_methods { add_child(System, element) }
484
+ end
485
+
486
+ def self.xpath
487
+ 'xccdf:ident'
488
+ end
489
+
490
+ def to_s
491
+ @children.map(&:to_s).join('|')
492
+ end
493
+ end
494
+
495
+ # Class for XCCDF rule complex check
496
+ class ComplexCheck < ElementBase
497
+ attr_reader :operator, :check
498
+
499
+ def initialize(element, parent: nil)
500
+ super
501
+ add_child(AttributeValue, element, 'operator')
502
+ add_children(Check, element)
503
+ end
504
+
505
+ def self.xpath
506
+ 'xccdf:complex-check'
507
+ end
508
+ end
509
+
510
+ # Class for XCCDF rule metadata cis_controls framework safeguard
511
+ class MetadataCisControlsFrameworkSafeguard < ElementBase
512
+ def initialize(element)
513
+ super
514
+ add_child(ShortText, element['title'])
515
+ add_child(ShortText, element['urn'])
516
+ new_implementation_groups(element)
517
+ add_child(ShortText, namespace_safe_at_xpath(element, 'controls:asset_type').text)
518
+ add_child(ShortText, namespace_safe_at_xpath(element, 'controls:security_function').text)
519
+ end
520
+
521
+ def self.xpath
522
+ 'controls:safeguard'
523
+ end
524
+
525
+ private
526
+
527
+ def new_implementation_groups(element)
528
+ igroup = namespace_safe_at_xpath(element, 'controls:implementation_groups')
529
+ add_child(ShortText, igroup['ig1']) if igroup['ig1']
530
+ add_child(ShortText, igroup['ig2']) if igroup['ig2']
531
+ add_child(ShortText, igroup['ig3']) if igroup['ig3']
532
+ end
533
+ end
534
+
535
+ # Class for XCCDF rule metadata cis_controls framework
536
+ class MetadataCisControlsFramework < ElementBase
537
+ def initialize(element)
538
+ super
539
+ add_child(AttributeValue, element, 'urn')
540
+ add_children(MetadataCisControlsFrameworkSafeguard, element)
541
+ end
542
+
543
+ def self.xpath
544
+ 'controls:framework'
545
+ end
546
+ end
547
+
548
+ # Class for XCCDF metadata cis_controls element
549
+ class MetadataCisControls < ElementBase
550
+ def initialize(element, parent: nil)
551
+ super
552
+ add_child(AttributeValue, element, 'xmlns:controls')
553
+ add_children(MetadataCisControlsFramework, element)
554
+ end
555
+
556
+ def self.xpath
557
+ 'controls:cis_controls'
558
+ end
559
+ end
560
+
561
+ # Class for XCCDF rule metadata element
562
+ class Metadata < ElementBase
563
+ def initialize(element, parent: nil)
564
+ super
565
+ add_children(MetadataCisControls, element)
566
+ end
567
+
568
+ def self.xpath
569
+ 'xccdf:metadata'
570
+ end
571
+ end
572
+
573
+ # Class for XCCDF Rule child element Rationale
574
+ class Rationale < ElementBase
575
+ def initialize(element)
576
+ super
577
+ add_child(LongText, element)
578
+ end
579
+
580
+ def digest
581
+ @digest ||= find_child_by_class(LongText).digest
582
+ end
583
+
584
+ def self.xpath
585
+ 'xccdf:rationale'
586
+ end
587
+
588
+ def to_s
589
+ find_child_by_class(LongText).to_s
590
+ end
591
+ end
592
+
593
+ # Class for XCCDF Rule child element Fixtext
594
+ class Fixtext < ElementBase
595
+ def initialize(element)
596
+ super
597
+ add_child(LongText, element)
598
+ end
599
+
600
+ def digest
601
+ @digest ||= find_child_by_class(LongText).digest
602
+ end
603
+
604
+ def self.xpath
605
+ 'xccdf:fixtext'
606
+ end
607
+
608
+ def to_s
609
+ find_child_by_class(LongText).to_s
610
+ end
611
+ end
612
+
613
+ # Class for XCCDF rule
614
+ class Rule < ElementWithId
615
+ include AbideDevUtils::XCCDF::Parser::Objects::NumberedObject
616
+ attr_reader :number
617
+
618
+ def initialize(element)
619
+ super
620
+ @number = to_s[/([0-9]+\.)+[0-9]+/]
621
+ add_child(AttributeValue, element, 'role')
622
+ add_child(AttributeValue, element, 'selected')
623
+ add_child(AttributeValue, element, 'weight')
624
+ add_child(Title, element)
625
+ add_child(Description, element)
626
+ add_child(Rationale, element)
627
+ add_children(Ident, element)
628
+ add_child(Fixtext, element)
629
+ add_children(Check, element)
630
+ add_child(ComplexCheck, element)
631
+ add_child(Metadata, element)
632
+ end
633
+
634
+ def self.xpath
635
+ 'xccdf:Rule'
636
+ end
637
+ end
638
+
639
+ # Class for XCCDF Value
640
+ class Value < ElementWithId
641
+ def initialize(element)
642
+ super
643
+ add_child(AttributeValue, element, 'operator')
644
+ add_child(AttributeValue, element, 'type')
645
+ add_child(Title, element)
646
+ add_child(Description, element)
647
+ add_child(ShortText, element.at_xpath('xccdf:value'))
648
+ end
649
+
650
+ def self.xpath
651
+ 'xccdf:Value'
652
+ end
653
+
654
+ def to_s
655
+ find_child_by_class(Title).to_s
656
+ end
657
+ end
658
+
659
+ # Class for XCCDF benchmark status
660
+ class Status < ElementBase
661
+ def initialize(element)
662
+ super
663
+ add_child(ShortText, element)
664
+ add_child(AttributeValue, element, 'date')
665
+ end
666
+
667
+ def self.xpath
668
+ 'xccdf:status'
669
+ end
670
+
671
+ def to_s
672
+ [
673
+ "Status:#{find_child_by_class(ShortText)}",
674
+ "Date:#{find_child_by_class(AttributeValue)}",
675
+ ].join('|')
676
+ end
677
+ end
678
+
679
+ # Class for XCCDF benchmark version
680
+ class Version < ElementBase
681
+ def initialize(element)
682
+ super
683
+ add_child(ShortText, element)
684
+ end
685
+
686
+ def self.xpath
687
+ 'xccdf:version'
688
+ end
689
+
690
+ def to_s
691
+ find_child_by_class(ShortText).to_s
692
+ end
693
+ end
694
+
695
+ # Class for XCCDF benchmark platform
696
+ class Platform < ElementBase
697
+ def initialize(element)
698
+ super
699
+ add_child(AttributeValue, element, 'idref')
700
+ end
701
+
702
+ def self.xpath
703
+ 'xccdf:platform'
704
+ end
705
+
706
+ def to_s
707
+ find_child_by_class(AttributeValue).to_s
708
+ end
709
+ end
710
+
711
+ # Class for XCCDF benchmark
712
+ class Benchmark < ElementBase
713
+ include AbideDevUtils::XCCDF::Parser::Objects::NumberedObject
714
+
715
+ def initialize(element)
716
+ super
717
+ element = element.at_xpath('xccdf:Benchmark')
718
+ raise 'No Benchmark element found' if element.nil?
719
+
720
+ add_child(Status, element)
721
+ add_child(Title, element)
722
+ add_child(Description, element)
723
+ add_child(Platform, element)
724
+ add_child(Version, element)
725
+ add_children(Profile, element)
726
+ add_children(Group, element)
727
+ add_children(Value, element)
728
+ end
729
+
730
+ def self.xpath
731
+ 'xccdf:Benchmark'
732
+ end
733
+
734
+ def to_s
735
+ [find_child_by_class(Title).to_s, find_child_by_class(Version).to_s].join(' ')
736
+ end
737
+ end
738
+ end
739
+ end
740
+ end
741
+ end