abide_dev_utils 0.14.2 → 0.15.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,267 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'abide_dev_utils/xccdf/diff/benchmark/number_title'
4
- require 'abide_dev_utils/xccdf/diff/benchmark/property'
5
- require 'abide_dev_utils/xccdf/diff/utils'
6
- require 'abide_dev_utils/xccdf/parser'
7
-
8
- module AbideDevUtils
9
- module XCCDF
10
- # Holds methods and classes used to diff XCCDF-derived objects.
11
- module Diff
12
- # Class for benchmark diffs
13
- class BenchmarkDiff
14
- include AbideDevUtils::XCCDF::Diff::BenchmarkPropertyDiff
15
- attr_reader :self, :other, :opts
16
-
17
- DEFAULT_OPTS = {
18
- only_classes: %w[rule],
19
- }.freeze
20
-
21
- # Used for filtering by level and profile
22
- LVL_PROF_DEFAULT = [nil, nil].freeze
23
-
24
- # @param xml1 [String] path to the first benchmark XCCDF xml file
25
- # @param xml2 [String] path to the second benchmark XCCDF xml file
26
- # @param opts [Hash] options hash
27
- def initialize(xml1, xml2, opts = {})
28
- @self = new_benchmark(xml1)
29
- @other = new_benchmark(xml2)
30
- @opts = DEFAULT_OPTS.merge(DEFAULT_PROPERTY_DIFF_OPTS).merge(opts)
31
- @levels = []
32
- @profiles = []
33
- end
34
-
35
- def method_missing(method_name, *args, &block)
36
- if opts.key?(method_name)
37
- opts[method_name]
38
- elsif @diff&.key?(method_name)
39
- @diff[method_name]
40
- else
41
- super
42
- end
43
- end
44
-
45
- def respond_to_missing?(method_name, include_private = false)
46
- opts.key?(method_name) || @diff&.key?(method_name) || super
47
- end
48
-
49
- # Memoized getter for all "numbered" children for the "self" benchmark based on optional filters in opts
50
- def self_numbered_children
51
- @self_numbered_children ||= find_all_numbered_children(@self,
52
- only_classes: opts[:only_classes],
53
- level: opts[:level],
54
- profile: opts[:profile])
55
- end
56
-
57
- # Memoized getter for all "numbered" children for the "other" benchmark based on optional filters in opts
58
- def other_numbered_children
59
- @other_numbered_children ||= find_all_numbered_children(@other,
60
- only_classes: opts[:only_classes],
61
- level: opts[:level],
62
- profile: opts[:profile])
63
- end
64
-
65
- # Basic title diff
66
- def numbered_children_title_diff
67
- {
68
- self: self_numbered_children.map { |c| c.title.to_s } - other_numbered_children.map { |c| c.title.to_s },
69
- other: other_numbered_children.map { |c| c.title.to_s } - self_numbered_children.map { |c| c.title.to_s },
70
- }
71
- end
72
-
73
- # Returns the output of a NumberTitleDiff object's diff function based on self_numbered_children and other_numbered_children
74
- def number_title_diff
75
- NumberTitleDiff.new(self_numbered_children, other_numbered_children).diff
76
- end
77
-
78
- # Hash of data about the "self" benchmark and the diff parameters
79
- def from_benchmark
80
- @from_benchmark ||= from_to_hash(@self)
81
- end
82
-
83
- # Hash of data about the "other" benchmark and the diff parameters
84
- def to_benchmark
85
- @to_benchmark ||= from_to_hash(@other)
86
- end
87
-
88
- # All levels that numbered children have been filtered on
89
- def levels
90
- @levels.flatten.uniq.empty? ? [:all] : @levels.flatten.uniq
91
- end
92
-
93
- # All profiles that numbered children have been filtered on
94
- def profiles
95
- @profiles.flatten.uniq.empty? ? [:all] : @profiles.flatten.uniq
96
- end
97
-
98
- # Returns a diff of the changes from the "self" xml (xml1) to the "other" xml (xml2)
99
- # This function is memoized because the diff operation is expensive. To run the diff
100
- # operation again, set the `new` parameter to `true`
101
- # @param new [Boolean] Set to `true` to force a new diff operation
102
- # return [Hash] the diff in hash format
103
- def diff(new: false)
104
- return @diff if @diff && !new
105
-
106
- @diff = {}
107
- @diff[:number_title] = number_title_diff
108
- { from: from_benchmark, to: to_benchmark, diff: @diff }
109
- end
110
-
111
- private
112
-
113
- # Returns a Benchmark object from a XCCDF xml file path
114
- # @param xml [String] path to a XCCDF xml file
115
- def new_benchmark(xml)
116
- AbideDevUtils::XCCDF::Parser.parse(xml)
117
- end
118
-
119
- # Returns a hash of benchmark data
120
- # @param obj [AbideDevUtils::XCCDF::Parser::Objects::Benchmark]
121
- # @return [Hash] diff-relevant benchmark information
122
- def from_to_hash(obj)
123
- {
124
- title: obj.title.to_s,
125
- version: obj.version.to_s,
126
- compared: {
127
- levels: levels,
128
- profiles: profiles,
129
- }
130
- }
131
- end
132
-
133
- # Function to check if a numbered child meets inclusion criteria based on filtering
134
- # options.
135
- # @param child [Object] XCCDF parser object that includes AbideDevUtils::XCCDF::Parser::Objects::NumberedObject.
136
- # @param only_classes [Array] class names as strings. When this is specified, only objects with those class names will be considered.
137
- # @param level [String] Specifies the benchmark profile level to filter children on. Only applies to Rules linked to Profiles that have levels.
138
- # @param profile [String] Specifies the benchmark profile to filter children on. Only applies to Rules that have linked Profiles.
139
- # @return [TrueClass] if child meets all filtering criteria and should be included in the set.
140
- # @return [FalseClass] if child does not meet all criteria and should be excluded from the set.
141
- def include_numbered_child?(child, only_classes: [], level: nil, profile: nil)
142
- return false unless valid_class?(child, only_classes)
143
- return true if level.nil? && profile.nil?
144
-
145
- validated_props = valid_profile_and_level(child, level, profile)
146
- should_include = validated_props.none?(&:nil?)
147
- new_validated_props_vars(validated_props) if should_include
148
- should_include
149
- end
150
-
151
- # Adds level and profile to respective instance vars
152
- # @param validated_props [Array] two item array: first item - profile level, second item - profile title
153
- def new_validated_props_vars(validated_props)
154
- @levels << validated_props[0]
155
- @profiles << validated_props[1]
156
- end
157
-
158
- # Checks if the child's class is in the only_classes list, if applicable
159
- # @param child [Object] the child whose class will be checked
160
- # @param only_classes [Array] an array of class names as strings
161
- # @return [TrueClass] if only_classes is empty or if child's class is in only_classes
162
- # @return [FalseClass] if only_classes is not empty and child's class is not in only_classes
163
- def valid_class?(child, only_classes = [])
164
- only_classes.empty? || only_classes.include?(child.label)
165
- end
166
-
167
- # Returns a two-item array of a valid level and a valid profile
168
- # @param child [Object] XCCDF parser object or array of XCCDF parser objects
169
- # @param level [String] a profile level
170
- # @param profile [String] a partial / full profile title
171
- # @return [Array] two-item array: first item - Profile level or nil, second item - Profile title or nil
172
- def valid_profile_and_level(child, level, profile)
173
- return LVL_PROF_DEFAULT unless child.respond_to?(:linked_profile)
174
-
175
- validate_profile_obj(child.linked_profile, level, profile)
176
- end
177
-
178
- # Returns array (or array of arrays) of valid level and valid profile based on child's linked profiles
179
- # @param obj [Object] AbideDevUtils::XCCDF::Parser::Objects::Profile objects or array of them
180
- # @param level [String] a profile level
181
- # @param profile [String] a partial / full profile title
182
- # @return [Array] two-item array: first item - Profile level or nil, second item - Profile title or nil
183
- # @return [Array] Array of two item arrays if `obj` is an array of profiles
184
- def validate_profile_obj(obj, level, profile)
185
- return LVL_PROF_DEFAULT if obj.nil?
186
- return validate_profile_objs(obj, level, profile) if obj.respond_to?(:each)
187
-
188
- validated_level = valid_level?(obj, level) ? obj.level : nil
189
- validated_profile = valid_profile?(obj, profile) ? obj.title.to_s : nil
190
- [validated_level, validated_profile]
191
- end
192
-
193
- # Returns array of arrays of valid levels and valid profiles based on all children's linked profiles
194
- # @param objs [Array] Array of AbideDevUtils::XCCDF::Parser::Objects::Profile objects
195
- # @param level [String] a profile level
196
- # @param profile [String] a partial / full profile title
197
- # @return [Array] Array of two-item arrays
198
- def validate_profile_objs(objs, level, profile)
199
- found = [LVL_PROF_DEFAULT]
200
- objs.each do |obj|
201
- validated = validate_profile_obj(obj, level, profile)
202
- next if validated.any?(&:nil?)
203
-
204
- found << validated
205
- end
206
- found
207
- end
208
-
209
- # Checks if a given object has a matching level. This is done
210
- # via a basic regex match on the object's #level method
211
- # @param obj [AbideDevUtils::XCCDF::Parser::Objects::Profile] the profile object
212
- # @param level [String] a level to check
213
- # @return [TrueClass] if level matches
214
- # @return [FalseClass] if level does not match
215
- def valid_level?(obj, level)
216
- return true if level.nil? || obj.level.nil?
217
-
218
- obj.level.match?(/#{Regexp.escape level}/i)
219
- end
220
-
221
- # Checks if a given profile object has a matching title or id. This
222
- # is done with a regex match against the profile's title as a string
223
- # or against it's object string representation (the ID).
224
- # @param obj [AbideDevUtils::XCCDF::Parser::Objects::Profile] the profile object
225
- # @param profile [String] a profile string to check
226
- # @return [TrueClass] if `profile` matches either the profile's title or ID
227
- # @return [FalseClass] if `profile` matches neither
228
- def valid_profile?(obj, profile)
229
- return true if profile.nil?
230
-
231
- obj.title.to_s.match?(/#{Regexp.escape profile}/i) ||
232
- obj.to_s.match?(/#{Regexp.escape profile}/i)
233
- end
234
-
235
- # Finds all children of the benchmark that implement AbideDevUtils::XCCDF::Parser::Objects::NumberedObject
236
- # that are not filtered out based on optional parameters. This method recursively walks down the hierarchy
237
- # of the benchmark to ensure that all deeply nested objects are accounted for.
238
- # @param benchmark [AbideDevUtils::XCCDF::Parser::Objects::Benchmark] the benchmark to check
239
- # @param only_classes [Array] An array of class names. Only children with the specified class names will be returned
240
- # @param level [String] A profile level. Only children that have linked profiles that match this level will be returned
241
- # @param profile [String] A profile title / id. Only children that have linked profiles that match this title / id will be returned
242
- # @param numbered_children [Array] An array of numbered children to check. If this is empty, we start checking at the top-level of
243
- # the benchmark. To get all of the benchmark's numbered children, this should be an empty array when calling this method.
244
- # @return [Array] A sorted array of numbered children.
245
- def find_all_numbered_children(benchmark, only_classes: [], level: nil, profile: nil, numbered_children: [])
246
- benchmark.find_children_that_respond_to(:number).each do |child|
247
- numbered_children << child if include_numbered_child?(child,
248
- only_classes: only_classes,
249
- level: level,
250
- profile: profile)
251
- find_all_numbered_children(child,
252
- only_classes: only_classes,
253
- level: level,
254
- profile: profile,
255
- numbered_children: numbered_children)
256
- end
257
- numbered_children.sort
258
- end
259
-
260
- # Returns a subset of benchmark children based on a property of that child and search values
261
- def find_subset_of_children(children, property, search_values)
262
- children.select { |c| search_values.include?(c.send(property)) }
263
- end
264
- end
265
- end
266
- end
267
- end
@@ -1,30 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module AbideDevUtils
4
- module XCCDF
5
- module Diff
6
- # Holds the result of a diff on a per-item basis.
7
- class DiffChangeResult
8
- attr_reader :type, :old_value, :new_value
9
-
10
- def initialize(type, old_value, new_value)
11
- @type = type
12
- @old_value = old_value
13
- @new_value = new_value
14
- end
15
-
16
- def to_h
17
- { type: type, old_value: old_value, new_value: new_value }
18
- end
19
-
20
- def to_a
21
- [type, old_value, new_value]
22
- end
23
-
24
- def to_s
25
- "#{type}: #{old_value} -> #{new_value}"
26
- end
27
- end
28
- end
29
- end
30
- end
@@ -1,118 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module AbideDevUtils
4
- module XCCDF
5
- module Parser
6
- module Objects
7
- # Methods for providing and comparing hash digests of objects
8
- module DigestObject
9
- # Excludes instance variables that are not used in the digest
10
- def exclude_from_digest(exclude)
11
- unless exclude.is_a?(Array) || exclude.is_a?(Symbol)
12
- raise ArgumentError, 'exclude must be an Array or Symbol'
13
- end
14
-
15
- @exclude_from_digest ||= []
16
- if exclude.is_a?(Array)
17
- exclude.map! do |e|
18
- normalize_exclusion(e)
19
- end
20
- @exclude_from_digest += exclude
21
- else
22
- @exclude_from_digest << normalize_exclusion(exclude)
23
- end
24
- @exclude_from_digest.uniq!
25
- end
26
-
27
- # Exclusions are instance variable symbols and must be prefixed with "@"
28
- def normalize_exclusion(exclude)
29
- exclude = "@#{exclude}" unless exclude.to_s.start_with?('@')
30
- exclude.to_sym
31
- end
32
-
33
- # Checks SHA256 digest equality
34
- def digest_equal?(other)
35
- digest == other.digest
36
- end
37
-
38
- # Returns a SHA256 digest of the object, including the digests of all
39
- # children
40
- def digest
41
- return @digest if defined?(@digest)
42
-
43
- parts = [labeled_self_digest]
44
- children.each { |child| parts << child.digest } unless children.empty?
45
- @digest = parts.join('|')
46
- @digest
47
- end
48
-
49
- # Returns a labeled digest of the current object
50
- def labeled_self_digest
51
- return "#{label}:#{Digest::SHA256.hexdigest(digestable_instance_variables)}" if respond_to?(:label)
52
-
53
- "none:#{Digest::SHA256.hexdigest(digestable_instance_variables)}"
54
- end
55
-
56
- # Returns a string of all instance variable values that are not nil, empty, or excluded
57
- def digestable_instance_variables
58
- instance_vars = instance_variables.reject { |iv| @exclude_from_digest.include?(iv) }.sort_by!(&:to_s)
59
- return 'empty' if instance_vars.empty?
60
-
61
- var_vals = instance_vars.map { |iv| instance_variable_get(iv) }
62
- var_vals.reject! { |v| v.nil? || v.empty? }
63
- return 'empty' if var_vals.empty?
64
-
65
- var_vals.join
66
- end
67
-
68
- # Compares two objects by their SHA256 digests
69
- # and returns the degree to which they are similar
70
- # as a percentage.
71
- def digest_similarity(other, only_labels: [], label_weights: {})
72
- digest_parts = sorted_digest_parts(digest)
73
- number_compared = 0
74
- cumulative_similarity = 0.0
75
- digest_parts.each do |digest_part|
76
- label, self_digest = split_labeled_digest(digest_part)
77
- next unless only_labels.empty? || only_labels.include?(label)
78
-
79
- label_weight = label_weights.key?(label) ? label_weights[label] : 1.0
80
- sorted_digest_parts(other.digest).each do |other_digest_part|
81
- other_label, other_digest = split_labeled_digest(other_digest_part)
82
- next unless (label == other_label) && (self_digest == other_digest)
83
-
84
- number_compared += 1
85
- cumulative_similarity += 1.0 * label_weight
86
- break # break when found
87
- end
88
- end
89
- cumulative_similarity / (number_compared.zero? ? 1.0 : number_compared)
90
- end
91
-
92
- def sorted_digest_parts(dgst)
93
- @sorted_digest_parts_cache = {} unless defined?(@sorted_digest_parts_cache)
94
- return @sorted_digest_parts_cache[dgst] if @sorted_digest_parts_cache.key?(dgst)
95
-
96
- @sorted_digest_parts_cache ||= {}
97
- @sorted_digest_parts_cache[dgst] = dgst.split('|').sort_by { |part| split_labeled_digest(part).first }
98
- @sorted_digest_parts_cache[dgst]
99
- end
100
-
101
- # If one of the digest parts is nil and the other is not, we can't compare
102
- def non_compatible?(digest_part, other_digest_part)
103
- (digest_part.nil? || other_digest_part.nil?) && digest_part != other_digest_part
104
- end
105
-
106
- # Splits a digest into a label and digest
107
- def split_labeled_digest(digest_part)
108
- @labeled_digest_part_cache = {} unless defined?(@labeled_digest_part_cache)
109
- return @labeled_digest_part_cache[digest_part] if @labeled_digest_part_cache.key?(digest_part)
110
-
111
- @labeled_digest_part_cache[digest_part] = digest_part.split(':')
112
- @labeled_digest_part_cache[digest_part]
113
- end
114
- end
115
- end
116
- end
117
- end
118
- end