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,232 +1,96 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'hashdiff'
3
+ require_relative 'parser'
4
4
 
5
5
  module AbideDevUtils
6
6
  module XCCDF
7
7
  # Contains methods and classes used to diff XCCDF-derived objects.
8
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
9
+ def self.benchmark_diff(xml1, xml2, opts = {})
10
+ bd = BenchmarkDiff.new(xml1, xml2, opts)
11
+ if opts[:raw]
12
+ return bd.diff_rules_raw if opts[:rules_only]
17
13
 
18
- # Represents a change in a diff.
19
- class ChangeSet
20
- attr_reader :change, :key, :value, :value_to
14
+ bd.diff_raw
15
+ else
16
+ return bd.diff_rules if opts[:rules_only]
21
17
 
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(', ')
18
+ bd.diff
111
19
  end
20
+ end
112
21
 
113
- def value_change_string_single_type(change_diff, value)
114
- "#{change_string(change_diff[0][0])} #{value[:number]} - #{value[:level]} - #{value[:title]}"
22
+ # Class for benchmark diffs
23
+ class BenchmarkDiff
24
+ attr_reader :this, :other, :opts
25
+
26
+ # @param xml1 [String] path to the first benchmark XCCDF xml file
27
+ # @param xml2 [String] path to the second benchmark XCCDF xml file
28
+ # @param opts [Hash] options hash
29
+ def initialize(xml1, xml2, opts = {})
30
+ @this = new_benchmark(xml1)
31
+ @other = new_benchmark(xml2)
32
+ @opts = opts
115
33
  end
116
34
 
117
- def all_single_change_type?(change_diff)
118
- change_diff.length > 1 && change_diff.map { |x| x[0] }.uniq.length == 1
35
+ def diff_raw
36
+ @diff_raw ||= @this.diff(@other)
119
37
  end
120
- end
121
38
 
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
39
+ def diff
40
+ warn 'Full benchmark diff is not yet implemented, return rules diff for now'
41
+ diff_rules
128
42
  end
129
43
 
130
- def diff
131
- @diff ||= new_diff
44
+ def diff_rules_raw
45
+ @diff_rules_raw ||= @this.diff_only_rules(@other)
132
46
  end
133
47
 
134
- private
48
+ def diff_rules
49
+ return diff_rules_raw if opts[:raw]
50
+ return [] if diff_rules_raw.all? { |r| r.type == :equal }
135
51
 
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
- )
52
+ diff_hash = {
53
+ from: @this.to_s,
54
+ to: @other.to_s,
55
+ rules: {}
56
+ }
57
+ diff_rules_raw.each do |rule|
58
+ diff_hash[:rules][rule.type] ||= []
59
+ case rule.type
60
+ when :added
61
+ diff_hash[:rules][rule.type] << { number: rule.new_value.number.to_s, title: rule.new_value.title.to_s }
62
+ when :removed
63
+ diff_hash[:rules][rule.type] << { number: rule.old_value.number.to_s, title: rule.old_value.title.to_s }
147
64
  else
148
- diff[change_key] = [ChangeSet.new(change: change[0], key: change[1], value: change[2], value_to: val_to)]
65
+ rd_hash = {}
66
+ rd_hash[:from] = "#{rule.old_value&.number} #{rule.old_value&.title}" if rule.old_value
67
+ rd_hash[:to] = "#{rule.new_value&.number} #{rule.new_value&.title}" if rule.new_value
68
+ changes = rule.details.transform_values { |v| v.is_a?(Array) ? v.map(&:to_s) : v.to_s }
69
+ if opts[:ignore_changed_properties]
70
+ changes.delete_if { |k, _| opts[:ignore_changed_properties].include?(k.to_s) }
71
+ next if changes.empty? # Skip entirely if all changed filtered out
72
+ end
73
+ rd_hash[:changes] = changes unless changes.empty?
74
+ diff_hash[:rules][rule.type] << rd_hash
149
75
  end
150
76
  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
77
+ unless opts[:no_stats]
78
+ stats_hash = {}
79
+ diff_hash[:rules].each do |type, rules|
80
+ stats_hash[type] = rules.size
81
+ end
82
+ diff_hash[:stats] = stats_hash unless stats_hash.empty?
160
83
  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)
84
+ diff_hash
182
85
  end
183
86
 
184
87
  private
185
88
 
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
89
+ # Returns a Benchmark object from a XCCDF xml file path
90
+ # @param xml [String] path to a XCCDF xml file
91
+ def new_benchmark(xml)
92
+ AbideDevUtils::XCCDF::Parser.parse(xml)
198
93
  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
94
  end
231
95
  end
232
96
  end
@@ -4,99 +4,6 @@ module AbideDevUtils
4
4
  module XCCDF
5
5
  module Parser
6
6
  module Helpers
7
- # Provides helper methods for working with XCCDF element children
8
- module ElementChildren
9
- def search_children
10
- @search_children ||= SearchChildren.new(children)
11
- end
12
-
13
- # Implements methods that allow for searching an XCCDF Element's children
14
- class SearchChildren
15
- attr_reader :children
16
-
17
- def initialize(children)
18
- @children = children
19
- end
20
-
21
- def recursive_select_children(children_to_search = children, &block)
22
- search_hits = []
23
- children_to_search.each do |child|
24
- found = yield child
25
- if found
26
- search_hits << child
27
- elsif child.respond_to?(:children)
28
- search_hits << recursive_select_children(child.children, &block)
29
- end
30
- end
31
- search_hits.flatten.compact.uniq
32
- end
33
-
34
- def recursive_find_child(children_to_search = children, &block)
35
- rescursive_select_children(children_to_search, &block).first
36
- end
37
-
38
- def find_children_that_respond_to(method, recurse: false)
39
- return recursive_select_children { |child| child.respond_to?(method) } if recurse
40
-
41
- children.select { |c| c.respond_to?(method.to_sym) }
42
- end
43
-
44
- def find_children_by_class(klass, recurse: false)
45
- return recursive_select_children { |child| child.instance_of?(klass) } if recurse
46
-
47
- children.select { |child| child.instance_of?(klass) }
48
- end
49
-
50
- def find_child_by_class(klass, recurse: false)
51
- return recursive_find_child { |child| child.is_a?(klass) } if recurse
52
-
53
- find_children_by_class(klass).first
54
- end
55
-
56
- def find_children_by_xpath(xpath, recurse: false)
57
- return recursive_select_children { |child| child.xpath == xpath } if recurse
58
-
59
- children.select { |child| child.xpath == xpath }
60
- end
61
-
62
- def find_child_by_xpath(xpath, recurse: false)
63
- return recursive_find_child { |child| child.xpath == xpath } if recurse
64
-
65
- find_children_by_xpath(xpath).first
66
- end
67
-
68
- def find_children_by_attribute(attribute, recurse: false)
69
- pr = proc do |child|
70
- next unless child.instance_of?(AbideDevUtils::XCCDF::Parser::Objects::AttributeValue)
71
-
72
- child.attribute == attribute
73
- end
74
- return recursive_select_children(&pr) if recurse
75
-
76
- children.select(&pr)
77
- end
78
-
79
- def find_child_by_attribute(attribute, recurse: false)
80
- find_children_by_attribute(attribute, recurse: recurse).first
81
- end
82
-
83
- def find_children_by_attribute_value(attribute, value, recurse: false)
84
- pr = proc do |child|
85
- next unless child.instance_of?(AbideDevUtils::XCCDF::Parser::Objects::AttributeValue)
86
-
87
- child.attribute == attribute && child.value == value
88
- end
89
- return recursive_select_children(&pr) if recurse
90
-
91
- children.select(&pr)
92
- end
93
-
94
- def find_child_by_attribute_value(attribute, value, recurse: false)
95
- find_children_by_attribute_value(attribute, value, recurse: recurse).first
96
- end
97
- end
98
- end
99
-
100
7
  # Provides helper methods for working with XML xpaths
101
8
  module XPath
102
9
  def find_element