abide_dev_utils 0.9.7 → 0.10.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +24 -8
- data/abide_dev_utils.gemspec +1 -0
- data/lib/abide_dev_utils/cem.rb +72 -0
- data/lib/abide_dev_utils/cli/cem.rb +73 -0
- data/lib/abide_dev_utils/cli/xccdf.rb +12 -1
- data/lib/abide_dev_utils/cli.rb +2 -0
- data/lib/abide_dev_utils/files.rb +34 -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 +9 -122
- data/new_diff.rb +48 -0
- metadata +34 -6
@@ -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
|
@@ -0,0 +1,104 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AbideDevUtils
|
4
|
+
module XCCDF
|
5
|
+
module Parser
|
6
|
+
module Objects
|
7
|
+
# Methods for interacting with objects that have numbers (e.g. Group, Rule, etc.)
|
8
|
+
# This module is included in the Benchmark class and Group / Rule classes
|
9
|
+
module NumberedObject
|
10
|
+
include ::Comparable
|
11
|
+
|
12
|
+
def <=>(other)
|
13
|
+
return 0 if number_eq(number, other.number)
|
14
|
+
return 1 if number_gt(number, other.number)
|
15
|
+
return -1 if number_lt(number, other.number)
|
16
|
+
end
|
17
|
+
|
18
|
+
def number_eq(this_num, other_num)
|
19
|
+
this_num == other_num
|
20
|
+
end
|
21
|
+
|
22
|
+
def number_parent_of?(this_num, other_num)
|
23
|
+
return false if number_eq(this_num, other_num)
|
24
|
+
|
25
|
+
# We split the numbers into parts and compare the resulting arrays
|
26
|
+
num1_parts = this_num.to_s.split('.')
|
27
|
+
num2_parts = other_num.to_s.split('.')
|
28
|
+
# For this_num to be a parent of other_num, the number of parts in
|
29
|
+
# this_num must be less than the number of parts in other_num.
|
30
|
+
# Additionally, each part of this_num must be equal to the parts of
|
31
|
+
# other_num at the same index.
|
32
|
+
# Example: this_num = '1.2.3' and other_num = '1.2.3.4'
|
33
|
+
# In this case, num1_parts = ['1', '2', '3'] and num2_parts = ['1', '2', '3', '4']
|
34
|
+
# So, this_num is a parent of other_num because at indexes 0, 1, and 2
|
35
|
+
# of num1_parts and num2_parts, the parts are equal.
|
36
|
+
num1_parts.length < num2_parts.length &&
|
37
|
+
num2_parts[0..(num1_parts.length - 1)] == num1_parts
|
38
|
+
end
|
39
|
+
|
40
|
+
def number_child_of?(this_num, other_num)
|
41
|
+
number_parent_of?(other_num, this_num)
|
42
|
+
end
|
43
|
+
|
44
|
+
def number_gt(this_num, other_num)
|
45
|
+
return false if number_eq(this_num, other_num)
|
46
|
+
return true if number_parent_of?(this_num, other_num)
|
47
|
+
|
48
|
+
num1_parts = this_num.to_s.split('.')
|
49
|
+
num2_parts = other_num.to_s.split('.')
|
50
|
+
num1_parts.zip(num2_parts).each do |num1_part, num2_part|
|
51
|
+
next if num1_part == num2_part # we skip past equal parts
|
52
|
+
|
53
|
+
# If num1_part is nil that means that we've had equal numbers so far.
|
54
|
+
# Therfore, this_num is greater than other num because of the
|
55
|
+
# hierarchical nature of the numbers.
|
56
|
+
# Example: this_num = '1.2' and other_num = '1.2.3'
|
57
|
+
# In this case, num1_part is nil and num2_part is '3'
|
58
|
+
# So, this_num is greater than other_num
|
59
|
+
return true if num1_part.nil?
|
60
|
+
# If num2_part is nil that means that we've had equal numbers so far.
|
61
|
+
# Therfore, this_num is less than other num because of the
|
62
|
+
# hierarchical nature of the numbers.
|
63
|
+
# Example: this_num = '1.2.3' and other_num = '1.2'
|
64
|
+
# In this case, num1_part is '3' and num2_part is nil
|
65
|
+
# So, this_num is less than other_num
|
66
|
+
return false if num2_part.nil?
|
67
|
+
|
68
|
+
return num1_part.to_i > num2_part.to_i
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def number_lt(this_num, other_num)
|
73
|
+
number_gt(other_num, this_num)
|
74
|
+
end
|
75
|
+
|
76
|
+
# This method will recursively walk the tree to find the first
|
77
|
+
# child, grandchild, etc. that has a number method and returns the
|
78
|
+
# matching number.
|
79
|
+
# @param [String] number The number to find in the tree
|
80
|
+
# @return [Group] The first child, grandchild, etc. that has a matching number
|
81
|
+
# @return [Rule] The first child, grandchild, etc. that has a matching number
|
82
|
+
# @return [nil] If no child, grandchild, etc. has a matching number
|
83
|
+
def search_children_by_number(number)
|
84
|
+
find_children_that_respond_to(:number).find do |child|
|
85
|
+
if number_eq(child.number, number)
|
86
|
+
child
|
87
|
+
elsif number_parent_of?(child.number, number)
|
88
|
+
# We recursively search the child for its child with the number
|
89
|
+
# if our number is a parent of the child's number
|
90
|
+
return child.search_children_by_number(number)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def find_child_by_number(number)
|
96
|
+
find_children_that_respond_to(:number).find do |child|
|
97
|
+
number_eq(child.number, number)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|