abide_dev_utils 0.9.7 → 0.11.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.rubocop.yml +7 -1
- data/Gemfile.lock +82 -64
- data/Rakefile +28 -0
- data/abide_dev_utils.gemspec +3 -1
- data/lib/abide_dev_utils/cem/benchmark.rb +291 -0
- data/lib/abide_dev_utils/cem/coverage_report.rb +348 -0
- data/lib/abide_dev_utils/cem/generate/reference.rb +116 -0
- data/lib/abide_dev_utils/cem/generate.rb +10 -0
- data/lib/abide_dev_utils/cem/mapping/mapper.rb +155 -0
- data/lib/abide_dev_utils/cem.rb +74 -0
- data/lib/abide_dev_utils/cli/cem.rb +153 -0
- data/lib/abide_dev_utils/cli/jira.rb +1 -1
- data/lib/abide_dev_utils/cli/xccdf.rb +15 -1
- data/lib/abide_dev_utils/cli.rb +2 -0
- data/lib/abide_dev_utils/errors/cem.rb +22 -0
- data/lib/abide_dev_utils/errors/general.rb +8 -2
- data/lib/abide_dev_utils/errors/ppt.rb +4 -0
- data/lib/abide_dev_utils/errors.rb +6 -0
- data/lib/abide_dev_utils/files.rb +34 -0
- data/lib/abide_dev_utils/markdown.rb +104 -0
- data/lib/abide_dev_utils/ppt/facter_utils.rb +140 -0
- data/lib/abide_dev_utils/ppt/hiera.rb +297 -0
- data/lib/abide_dev_utils/ppt/puppet_module.rb +74 -0
- data/lib/abide_dev_utils/ppt.rb +3 -5
- data/lib/abide_dev_utils/validate.rb +14 -0
- data/lib/abide_dev_utils/version.rb +1 -1
- data/lib/abide_dev_utils/xccdf/diff/benchmark/number_title.rb +270 -0
- data/lib/abide_dev_utils/xccdf/diff/benchmark/profile.rb +104 -0
- data/lib/abide_dev_utils/xccdf/diff/benchmark/property.rb +127 -0
- data/lib/abide_dev_utils/xccdf/diff/benchmark/property_existence.rb +47 -0
- data/lib/abide_dev_utils/xccdf/diff/benchmark.rb +267 -0
- data/lib/abide_dev_utils/xccdf/diff/utils.rb +30 -0
- data/lib/abide_dev_utils/xccdf/diff.rb +233 -0
- data/lib/abide_dev_utils/xccdf/parser/objects/digest_object.rb +118 -0
- data/lib/abide_dev_utils/xccdf/parser/objects/numbered_object.rb +104 -0
- data/lib/abide_dev_utils/xccdf/parser/objects.rb +741 -0
- data/lib/abide_dev_utils/xccdf/parser.rb +52 -0
- data/lib/abide_dev_utils/xccdf.rb +14 -124
- data/new_diff.rb +48 -0
- metadata +60 -9
- data/lib/abide_dev_utils/ppt/coverage.rb +0 -86
@@ -0,0 +1,267 @@
|
|
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
|
@@ -0,0 +1,30 @@
|
|
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
|
@@ -0,0 +1,233 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'hashdiff'
|
4
|
+
|
5
|
+
module AbideDevUtils
|
6
|
+
module XCCDF
|
7
|
+
# Contains methods and classes used to diff XCCDF-derived objects.
|
8
|
+
module Diff
|
9
|
+
DEFAULT_DIFF_OPTS = {
|
10
|
+
similarity: 1,
|
11
|
+
strict: true,
|
12
|
+
strip: true,
|
13
|
+
array_path: true,
|
14
|
+
delimiter: '//',
|
15
|
+
use_lcs: true,
|
16
|
+
}.freeze
|
17
|
+
|
18
|
+
# Represents a change in a diff.
|
19
|
+
class ChangeSet
|
20
|
+
attr_reader :change, :key, :value, :value_to
|
21
|
+
|
22
|
+
def initialize(change:, key:, value:, value_to: nil)
|
23
|
+
validate_change(change)
|
24
|
+
@change = change
|
25
|
+
@key = key
|
26
|
+
@value = value
|
27
|
+
@value_to = value_to
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_s
|
31
|
+
value_change_string(value, value_to)
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_h
|
35
|
+
{
|
36
|
+
change: change,
|
37
|
+
key: key,
|
38
|
+
value: value,
|
39
|
+
value_to: value_to,
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
43
|
+
def can_merge?(other)
|
44
|
+
return false unless (change == '-' && other.change == '+') || (change == '+' && other.change == '-')
|
45
|
+
return false unless key == other.key || value_hash_equality(other)
|
46
|
+
|
47
|
+
true
|
48
|
+
end
|
49
|
+
|
50
|
+
def merge(other)
|
51
|
+
unless can_merge?(other)
|
52
|
+
raise ArgumentError, 'Cannot merge. Possible causes: change is identical; key or value do not match'
|
53
|
+
end
|
54
|
+
|
55
|
+
new_to_value = value == other.value ? nil : other.value
|
56
|
+
ChangeSet.new(
|
57
|
+
change: '~',
|
58
|
+
key: key,
|
59
|
+
value: value,
|
60
|
+
value_to: new_to_value
|
61
|
+
)
|
62
|
+
end
|
63
|
+
|
64
|
+
def merge!(other)
|
65
|
+
new_props = merge(other)
|
66
|
+
@change = new_props.change
|
67
|
+
@key = new_props.key
|
68
|
+
@value = new_props.value
|
69
|
+
@value_to = new_props.value_to
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def value_hash_equality(other)
|
75
|
+
equality = false
|
76
|
+
value.each do |k, v|
|
77
|
+
equality = true if v == other.value[k]
|
78
|
+
end
|
79
|
+
equality
|
80
|
+
end
|
81
|
+
|
82
|
+
def validate_change(chng)
|
83
|
+
raise ArgumentError, "Change type #{chng} in invalid" unless ['+', '-', '~'].include?(chng)
|
84
|
+
end
|
85
|
+
|
86
|
+
def change_string(chng)
|
87
|
+
case chng
|
88
|
+
when '-'
|
89
|
+
'Remove'
|
90
|
+
when '+'
|
91
|
+
'Add'
|
92
|
+
else
|
93
|
+
'Change'
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def value_change_string(value, value_to)
|
98
|
+
change_str = []
|
99
|
+
change_diff = Hashdiff.diff(value, value_to || {}, AbideDevUtils::XCCDF::Diff::DEFAULT_DIFF_OPTS)
|
100
|
+
return if change_diff.empty?
|
101
|
+
return value_change_string_single_type(change_diff, value) if all_single_change_type?(change_diff)
|
102
|
+
|
103
|
+
change_diff.each do |chng|
|
104
|
+
change_str << if chng.length == 4
|
105
|
+
"#{change_string(chng[0])} #{chng[1][0]} \"#{chng[2]}\" to \"#{chng[3]}\""
|
106
|
+
else
|
107
|
+
"#{change_string(chng[0])} #{chng[1][0]} with value #{chng[2]}"
|
108
|
+
end
|
109
|
+
end
|
110
|
+
change_str.join(', ')
|
111
|
+
end
|
112
|
+
|
113
|
+
def value_change_string_single_type(change_diff, value)
|
114
|
+
"#{change_string(change_diff[0][0])} #{value[:number]} - #{value[:level]} - #{value[:title]}"
|
115
|
+
end
|
116
|
+
|
117
|
+
def all_single_change_type?(change_diff)
|
118
|
+
change_diff.length > 1 && change_diff.map { |x| x[0] }.uniq.length == 1
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# Class used to diff two Benchmark profiles.
|
123
|
+
class ProfileDiff
|
124
|
+
def initialize(profile_a, profile_b, opts = {})
|
125
|
+
@profile_a = profile_a
|
126
|
+
@profile_b = profile_b
|
127
|
+
@opts = opts
|
128
|
+
end
|
129
|
+
|
130
|
+
def diff
|
131
|
+
@diff ||= new_diff
|
132
|
+
end
|
133
|
+
|
134
|
+
private
|
135
|
+
|
136
|
+
def new_diff
|
137
|
+
Hashdiff.diff(@profile_a, @profile_b, AbideDevUtils::XCCDF::Diff::DEFAULT_DIFF_OPTS).each_with_object({}) do |change, diff|
|
138
|
+
val_to = change.length == 4 ? change[3] : nil
|
139
|
+
change_key = change[2].is_a?(Hash) ? change[2][:title] : change[2]
|
140
|
+
if diff.key?(change_key)
|
141
|
+
diff[change_key] = merge_changes(
|
142
|
+
[
|
143
|
+
diff[change_key][0],
|
144
|
+
ChangeSet.new(change: change[0], key: change[1], value: change[2], value_to: val_to),
|
145
|
+
]
|
146
|
+
)
|
147
|
+
else
|
148
|
+
diff[change_key] = [ChangeSet.new(change: change[0], key: change[1], value: change[2], value_to: val_to)]
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def merge_changes(changes)
|
154
|
+
return changes if changes.length < 2
|
155
|
+
|
156
|
+
if changes[0].can_merge?(changes[1])
|
157
|
+
[changes[0].merge(changes[1])]
|
158
|
+
else
|
159
|
+
changes
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
# Class used to diff two AbideDevUtils::XCCDF::Benchmark objects.
|
165
|
+
class BenchmarkDiff
|
166
|
+
def initialize(benchmark_a, benchmark_b, opts = {})
|
167
|
+
@benchmark_a = benchmark_a
|
168
|
+
@benchmark_b = benchmark_b
|
169
|
+
@opts = opts
|
170
|
+
end
|
171
|
+
|
172
|
+
def properties_to_diff
|
173
|
+
@properties_to_diff ||= %i[title version profiles]
|
174
|
+
end
|
175
|
+
|
176
|
+
def title_version
|
177
|
+
@title_version ||= diff_title_version
|
178
|
+
end
|
179
|
+
|
180
|
+
def profiles(profile: nil)
|
181
|
+
@profiles ||= diff_profiles(profile: profile)
|
182
|
+
end
|
183
|
+
|
184
|
+
private
|
185
|
+
|
186
|
+
def diff_title_version
|
187
|
+
diff = Hashdiff.diff(
|
188
|
+
@benchmark_a.to_h.reject { |k, _| k.to_s == 'profiles' },
|
189
|
+
@benchmark_b.to_h.reject { |k, _| k.to_s == 'profiles' },
|
190
|
+
AbideDevUtils::XCCDF::Diff::DEFAULT_DIFF_OPTS
|
191
|
+
)
|
192
|
+
diff.each_with_object({}) do |change, tdiff|
|
193
|
+
val_to = change.length == 4 ? change[3] : nil
|
194
|
+
change_key = change[2].is_a?(Hash) ? change[2][:title] : change[2]
|
195
|
+
tdiff[change_key] = [] unless tdiff.key?(change_key)
|
196
|
+
tdiff[change_key] << ChangeSet.new(change: change[0], key: change[1], value: change[2], value_to: val_to)
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
def diff_profiles(profile: nil)
|
201
|
+
diff = {}
|
202
|
+
other_hash = @benchmark_b.to_h[:profiles]
|
203
|
+
@benchmark_a.to_h[:profiles].each do |name, data|
|
204
|
+
next if profile && profile != name
|
205
|
+
|
206
|
+
diff[name] = ProfileDiff.new(data, other_hash[name], @opts).diff
|
207
|
+
end
|
208
|
+
diff
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
def self.diff_benchmarks(benchmark_a, benchmark_b, opts = {})
|
213
|
+
profile = opts.fetch(:profile, nil)
|
214
|
+
profile_key = profile.nil? ? 'all_profiles' : profile
|
215
|
+
benchmark_diff = BenchmarkDiff.new(benchmark_a, benchmark_b, opts)
|
216
|
+
transform_method_sym = opts.fetch(:raw, false) ? :to_h : :to_s
|
217
|
+
diff = if profile.nil?
|
218
|
+
benchmark_diff.profiles.each do |_, v|
|
219
|
+
v.transform_values! { |x| x.map!(&transform_method_sym) }
|
220
|
+
end
|
221
|
+
else
|
222
|
+
benchmark_diff.profiles(profile: profile)[profile].transform_values! { |x| x.map!(&transform_method_sym) }
|
223
|
+
end
|
224
|
+
return diff.values.flatten if opts.fetch(:raw, false)
|
225
|
+
|
226
|
+
{
|
227
|
+
'benchmark' => benchmark_diff.title_version.transform_values { |x| x.map!(&:to_s) },
|
228
|
+
profile_key => diff.values.flatten,
|
229
|
+
}
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
@@ -0,0 +1,118 @@
|
|
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
|