abide_dev_utils 0.9.5 → 0.10.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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