abide_dev_utils 0.14.1 → 0.15.0

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