compliance_engine 0.1.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.
data/TODO.md ADDED
@@ -0,0 +1,19 @@
1
+ Unimplemented features, in no particular order:
2
+
3
+ - [x] Limit ces/checks/controls based on selected profile
4
+ - [x] Correlation between ces/controls and checks
5
+ - [ ] Test merge of profiles (ordering of settings)
6
+ - [ ] Test malformed data
7
+ - [ ] Storage and resolution of facts
8
+ - [ ] Confinement
9
+ - [ ] Enforcement tolerance
10
+ - [ ] Hiera backend functionality
11
+ - [ ] Add missing documentation
12
+ - [ ] Reset state when files are updated
13
+ - [ ] Shared state between multiple objects (store file data)
14
+ - [ ] Command-line tools for examining compliance data
15
+ - [ ] Lint support (replace `scelint`)
16
+ - [ ] Resolve oval ids to CEs
17
+ - [ ] Puppet environment support
18
+ - [ ] Read/store metadata
19
+ - [ ] Load compliance data from a module path
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'compliance_engine/cli'
5
+
6
+ ComplianceEngine::CLI.start(ARGV)
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'compliance_engine'
4
+
5
+ # A compliance engine data CE
6
+ class ComplianceEngine::Ce < ComplianceEngine::Component
7
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'compliance_engine'
4
+
5
+ # A collection of compliance engine data CEs
6
+ class ComplianceEngine::Ces < ComplianceEngine::Collection
7
+ # A Hash of CEs by OVAL ID
8
+ #
9
+ # @return [Hash]
10
+ def by_oval_id
11
+ return @by_oval_id unless @by_oval_id.nil?
12
+
13
+ @by_oval_id ||= {}
14
+
15
+ each do |k, v|
16
+ v.oval_ids&.each do |oval_id|
17
+ @by_oval_id[oval_id] ||= {}
18
+ @by_oval_id[oval_id][k] = v
19
+ end
20
+ end
21
+
22
+ @by_oval_id
23
+ end
24
+
25
+ private
26
+
27
+ # Returns the key of the collection in compliance engine source data
28
+ #
29
+ # @return [String]
30
+ def key
31
+ 'ce'
32
+ end
33
+
34
+ # Returns the class to use for the collection
35
+ #
36
+ # @return [Class]
37
+ def collected
38
+ ComplianceEngine::Ce
39
+ end
40
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'compliance_engine'
4
+
5
+ # A compliance engine data check
6
+ class ComplianceEngine::Check < ComplianceEngine::Component
7
+ # Returns the settings of the check
8
+ #
9
+ # @return [Hash] the settings of the check
10
+ def settings
11
+ element['settings']
12
+ end
13
+
14
+ # Returns the Puppet class parameters of the check
15
+ #
16
+ # @return [Hash] the Puppet class parameters of the check
17
+ def hiera
18
+ return @hiera unless @hiera.nil?
19
+
20
+ return @hiera = nil unless type == 'puppet-class-parameter'
21
+
22
+ @hiera = { settings['parameter'] => settings['value'] }
23
+ end
24
+
25
+ # Returns the type of the check
26
+ #
27
+ # @return [String] the type of the check
28
+ def type
29
+ element['type']
30
+ end
31
+
32
+ # Returns the remediation data of the check
33
+ #
34
+ # @return [Hash] the remediation data of the check
35
+ def remediation
36
+ element['remediation']
37
+ end
38
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'compliance_engine'
4
+
5
+ # A collection of compliance engine data checks
6
+ class ComplianceEngine::Checks < ComplianceEngine::Collection
7
+ private
8
+
9
+ # Returns the key of the collection in compliance engine source data
10
+ #
11
+ # @return [String]
12
+ def key
13
+ 'checks'
14
+ end
15
+
16
+ # Returns the class to use for the collection
17
+ #
18
+ # @return [Class]
19
+ def collected
20
+ ComplianceEngine::Check
21
+ end
22
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'compliance_engine'
4
+ require 'thor'
5
+
6
+ # Compliance Engine CLI
7
+ class ComplianceEngine::CLI < Thor
8
+ class_option :facts, type: :string
9
+ class_option :enforcement_tolerance, type: :numeric
10
+ class_option :module, type: :array, default: []
11
+ class_option :modulepath, type: :array
12
+ class_option :modulezip, type: :string
13
+
14
+ desc 'hiera', 'Dump Hiera data'
15
+ option :profile, type: :array, required: true
16
+ def hiera
17
+ require 'yaml'
18
+ puts data.hiera(options[:profile]).to_yaml
19
+ end
20
+
21
+ desc 'lookup KEY', 'Look up a Hiera key'
22
+ option :profile, type: :array, required: true
23
+ def lookup(key)
24
+ require 'yaml'
25
+ puts data.hiera(options[:profile]).select { |k, _| k == key }.to_yaml
26
+ end
27
+
28
+ desc 'dump', 'Dump all compliance data'
29
+ def dump
30
+ require 'yaml'
31
+ data.files.each do |file|
32
+ puts({ file => data.get(file) }.to_yaml)
33
+ end
34
+ end
35
+
36
+ desc 'profiles', 'List available profiles'
37
+ def profiles
38
+ require 'yaml'
39
+ puts data.profiles.select { |_, value| value.ces&.count&.positive? || value.controls&.count&.positive? }.keys.to_yaml
40
+ end
41
+
42
+ desc 'inspect', 'Start an interactive shell'
43
+ def inspect
44
+ # Run the CLI with `data` as the object containing the compliance data.
45
+ require 'irb'
46
+ # rubocop:disable Lint/Debugger
47
+ binding.irb
48
+ # rubocop:enable Lint/Debugger
49
+ end
50
+
51
+ private
52
+
53
+ def data
54
+ return @data unless @data.nil?
55
+
56
+ @data = ComplianceEngine::Data.new(facts: facts, enforcement_tolerance: options[:enforcement_tolerance])
57
+ if options[:modulezip]
58
+ @data.open_environment_zip(options[:modulezip])
59
+ elsif options[:modulepath]
60
+ @data.open_environment(*options[:modulepath])
61
+ else
62
+ @data.open(*options[:module])
63
+ end
64
+
65
+ @data
66
+ end
67
+
68
+ def facts
69
+ return nil unless options[:facts]
70
+ return @facts unless @facts.nil?
71
+
72
+ require 'json'
73
+
74
+ @facts = JSON.parse(options[:facts])
75
+ end
76
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'compliance_engine'
4
+
5
+ # A generic compliance engine data collection
6
+ class ComplianceEngine::Collection
7
+ # A generic compliance engine data collection
8
+ #
9
+ # @param data [ComplianceEngine::Data] the data to initialize the object with
10
+ def initialize(data)
11
+ context_variables.each { |var| instance_variable_set(var, data.instance_variable_get(var)) }
12
+ @collection ||= {}
13
+ hash_key = key
14
+ data.files.each do |file|
15
+ data.get(file)[hash_key]&.each do |k, v|
16
+ @collection[k] ||= collected.new(k, data: self)
17
+ @collection[k].add(file, v)
18
+ end
19
+ end
20
+ end
21
+
22
+ attr_accessor :collection, :facts, :enforcement_tolerance, :environment_data
23
+
24
+ # Invalidate the cache of computed data
25
+ #
26
+ # @param data [ComplianceEngine::Data, NilClass] the data to initialize the object with
27
+ # @return [NilClass]
28
+ def invalidate_cache(data = nil)
29
+ context_variables.each { |var| instance_variable_set(var, data&.instance_variable_get(var)) }
30
+ collection.each_value { |obj| obj.invalidate_cache(data) }
31
+ (instance_variables - (context_variables + [:@collection])).each { |var| instance_variable_set(var, nil) }
32
+ nil
33
+ end
34
+
35
+ # Converts the object to a hash representation
36
+ #
37
+ # @return [Hash] the hash representation of the object
38
+ def to_h
39
+ collection.reject { |k, _| k.is_a?(Symbol) }
40
+ end
41
+
42
+ # Returns the keys of the collection
43
+ #
44
+ # @return [Array] the keys of the collection
45
+ def keys
46
+ to_h.keys
47
+ end
48
+
49
+ # Return a single value from the collection
50
+ #
51
+ # @param key [String] the key of the value to return
52
+ # @return [Object] the value of the key
53
+ def [](key)
54
+ collection[key]
55
+ end
56
+
57
+ # Iterates over the collection
58
+ #
59
+ # @param block [Proc] the block to execute
60
+ def each(&block)
61
+ to_h.each(&block)
62
+ end
63
+
64
+ # Iterates over values in the collection
65
+ #
66
+ # @param block [Proc] the block to execute
67
+ def each_value(&block)
68
+ to_h.each_value(&block)
69
+ end
70
+
71
+ # Iterates over keys in the collection
72
+ #
73
+ # @param block [Proc] the block to execute
74
+ def each_key(&block)
75
+ to_h.each_key(&block)
76
+ end
77
+
78
+ # Return true if any of the values in the collection match the block
79
+ #
80
+ # @param block [Proc] the block to execute
81
+ # @return [TrueClass, FalseClass] true if any of the values in the collection match the block
82
+ def any?(&block)
83
+ to_h.any?(&block)
84
+ end
85
+
86
+ # Return true if all of the values in the collection match the block
87
+ #
88
+ # @param block [Proc] the block to execute
89
+ # @return [TrueClass, FalseClass] true if all of the values in the collection match the block
90
+ def all?(&block)
91
+ to_h.all?(&block)
92
+ end
93
+
94
+ # Select values in the collection
95
+ #
96
+ # @param block [Proc] the block to execute
97
+ # @return [Hash] the filtered hash
98
+ def select(&block)
99
+ to_h.select(&block)
100
+ end
101
+
102
+ # Filter out values in the collection
103
+ #
104
+ # @param block [Proc] the block to execute
105
+ # @return [Hash] the filtered hash
106
+ def reject(&block)
107
+ to_h.reject(&block)
108
+ end
109
+
110
+ private
111
+
112
+ # Get the context variables
113
+ #
114
+ # @return [Array<Symbol>]
115
+ def context_variables
116
+ [:@enforcement_tolerance, :@environment_data, :@facts]
117
+ end
118
+
119
+ # Returns the key of the object
120
+ #
121
+ # @return [NotImplementedError] This method is not implemented and should be overridden by subclasses.
122
+ def key
123
+ raise NotImplementedError
124
+ end
125
+
126
+ # Returns the class to use for the collection
127
+ #
128
+ # @return [NotImplementedError] This method is not implemented and should be overridden by subclasses.
129
+ def collected
130
+ raise NotImplementedError
131
+ end
132
+ end
@@ -0,0 +1,251 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'compliance_engine'
4
+ require 'deep_merge'
5
+
6
+ # A generic compliance engine data component
7
+ class ComplianceEngine::Component
8
+ # A generic compliance engine data component
9
+ #
10
+ # @param [String] component The component key
11
+ # @param [ComplianceEngine::Data, ComplianceEngine::Collection, NilClass] data The data to initialize the object with
12
+ def initialize(name, data: nil)
13
+ context_variables.each { |var| instance_variable_set(var, data&.instance_variable_get(var)) }
14
+ @component ||= { key: name, fragments: {} }
15
+ end
16
+
17
+ attr_accessor :component, :facts, :enforcement_tolerance, :environment_data
18
+
19
+ # Invalidate the cache of computed data
20
+ #
21
+ # @param data [ComplianceEngine::Data, ComplianceEngine::Collection, NilClass] the data to initialize the object with
22
+ # @return [NilClass]
23
+ def invalidate_cache(data = nil)
24
+ context_variables.each { |var| instance_variable_set(var, data&.instance_variable_get(var)) }
25
+ cache_variables.each { |var| instance_variable_set(var, nil) }
26
+ nil
27
+ end
28
+
29
+ # Adds a value to the fragments array of the component.
30
+ #
31
+ # @param value [Object] The value to be added to the fragments array.
32
+ # @return [Object]
33
+ def add(filename, value)
34
+ component[:fragments][filename] = value
35
+ end
36
+
37
+ # Returns an array of fragments from the component
38
+ #
39
+ # @return [Array] an array of fragments
40
+ def to_a
41
+ component[:fragments].values
42
+ end
43
+
44
+ # Returns the merged data from the component
45
+ #
46
+ # @return [Hash] merged data
47
+ def to_h
48
+ element
49
+ end
50
+
51
+ # Returns the key of the component
52
+ #
53
+ # @return [String] the key of the component
54
+ def key
55
+ component[:key]
56
+ end
57
+
58
+ # Returns the title of the component
59
+ #
60
+ # @return [String] the title of the component
61
+ def title
62
+ element['title']
63
+ end
64
+
65
+ # Returns the description of the component
66
+ #
67
+ # @return [String] the description of the component
68
+ def description
69
+ element['description']
70
+ end
71
+
72
+ # Returns the oval ids of the component
73
+ #
74
+ # @return [Array] the oval ids of the component
75
+ def oval_ids
76
+ element['oval-ids']
77
+ end
78
+
79
+ # Returns the controls of the component
80
+ #
81
+ # @return [Hash] the controls of the component
82
+ def controls
83
+ element['controls']
84
+ end
85
+
86
+ # Returns the identifiers of the component
87
+ #
88
+ # @return [Hash] the identifiers of the component
89
+ def identifiers
90
+ element['identifiers']
91
+ end
92
+
93
+ # Returns the ces of the component
94
+ #
95
+ # @return [Array, Hash] the ces of the component
96
+ # @note This returns an Array for checks and a Hash for other components
97
+ def ces
98
+ element['ces']
99
+ end
100
+
101
+ # Return a single key from the component
102
+ #
103
+ # @param key [String] the key of the value to return
104
+ # @return [Object] the value of the key
105
+ def [](key)
106
+ element[key]
107
+ end
108
+
109
+ private
110
+
111
+ # Get the context variables
112
+ #
113
+ # @return [Array<Symbol>]
114
+ def context_variables
115
+ [:@enforcement_tolerance, :@environment_data, :@facts]
116
+ end
117
+
118
+ # Get the cache variables
119
+ #
120
+ # @return [Array<Symbol>]
121
+ def cache_variables
122
+ instance_variables - (context_variables + [:@component])
123
+ end
124
+
125
+ # Compare a fact value against a confine value
126
+ #
127
+ # @param [Object] fact The fact value
128
+ # @param [Object] confine The confine value
129
+ # @param [Integer] depth The depth of the recursion
130
+ # @return [TrueClass, FalseClass] true if the fact value matches the confine value
131
+ def fact_match?(fact, confine, depth = 0)
132
+ if confine.is_a?(String)
133
+ return fact != confine.delete_prefix('!') if confine.start_with?('!')
134
+
135
+ fact == confine
136
+ elsif confine.is_a?(Array)
137
+ if depth == 0
138
+ confine.any? { |value| fact_match?(fact, value, depth + 1) }
139
+ else
140
+ fact == confine
141
+ end
142
+ else
143
+ fact == confine
144
+ end
145
+ end
146
+
147
+ # Check if a fragment is confined
148
+ #
149
+ # @param [Hash] fragment The fragment to check
150
+ # @return [TrueClass, FalseClass] true if the fragment should be dropped
151
+ def confine_away?(fragment)
152
+ return false unless fragment.key?('confine')
153
+
154
+ fragment['confine'].each do |k, v|
155
+ if k == 'module_name'
156
+ unless environment_data.nil?
157
+ return true unless environment_data.key?(v)
158
+ module_version = fragment['confine']['module_version']
159
+ unless module_version.nil?
160
+ require 'semantic_puppet'
161
+ begin
162
+ return true unless SemanticPuppet::VersionRange.parse(module_version).include?(SemanticPuppet::Version.parse(environment_data[v]))
163
+ rescue => e
164
+ warn "Failed to compare #{v} #{environment_data[v]} with version confinement #{module_version}: #{e.message}"
165
+ return true
166
+ end
167
+ end
168
+ end
169
+ elsif k == 'module_version'
170
+ warn "Missing module name for #{fragment}" unless fragment['confine'].key?('module_name')
171
+ else
172
+ # Confinement based on Puppet facts
173
+ unless facts.nil?
174
+ fact = facts.dig(*k.split('.'))
175
+ if fact.nil?
176
+ return true
177
+ end
178
+ unless fact_match?(fact, v)
179
+ return true
180
+ end
181
+ end
182
+ end
183
+ end
184
+
185
+ false
186
+ end
187
+
188
+ # Returns the fragments of the component after confinement
189
+ #
190
+ # @return [Hash] the fragments of the component
191
+ def fragments
192
+ return @fragments unless @fragments.nil?
193
+
194
+ @fragments ||= {}
195
+
196
+ component[:fragments].each do |filename, fragment|
197
+ # If none of the confinable data is present in the object,
198
+ # ignore confinement data entirely.
199
+ if facts.nil? && enforcement_tolerance.nil? && environment_data.nil?
200
+ @fragments[filename] = fragment
201
+ next
202
+ end
203
+
204
+ # If no confine data is present in the fragment, include it.
205
+ if !fragment.key?('confine') && !fragment.key?('remediation')
206
+ @fragments[filename] = fragment
207
+ next
208
+ end
209
+
210
+ next if confine_away?(fragment)
211
+
212
+ # Confinement based on remediation risk
213
+ if enforcement_tolerance.is_a?(Integer) && is_a?(ComplianceEngine::Check) && fragment.key?('remediation')
214
+ if fragment['remediation'].key?('disabled')
215
+ message = "Remediation disabled for #{fragment}"
216
+ reason = fragment['remediation']['disabled']&.map { |value| value['reason'] }&.reject { |value| value.nil? }&.join("\n")
217
+ message += "\n#{reason}" unless reason.nil?
218
+ warn message
219
+ next
220
+ end
221
+
222
+ if fragment['remediation'].key?('risk')
223
+ risk_level = fragment['remediation']['risk']&.map { |value| value['level'] }&.select { |value| value.is_a?(Integer) }&.max
224
+ if risk_level.is_a?(Integer) && risk_level >= enforcement_tolerance
225
+ warn "Remediation risk #{risk_level} exceeds enforcement tolerance #{enforcement_tolerance} for #{fragment}"
226
+ next
227
+ end
228
+ end
229
+ end
230
+
231
+ @fragments[filename] = fragment
232
+ end
233
+
234
+ @fragments
235
+ end
236
+
237
+ # Returns a merged view of the component fragments
238
+ #
239
+ # @return [Object] the element of the component
240
+ def element
241
+ return @element unless @element.nil?
242
+
243
+ @element = {}
244
+
245
+ fragments.each_value do |fragment|
246
+ @element = @element.deep_merge!(fragment)
247
+ end
248
+
249
+ @element
250
+ end
251
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'compliance_engine'
4
+
5
+ # A compliance engine data control
6
+ class ComplianceEngine::Control < ComplianceEngine::Component
7
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'compliance_engine'
4
+
5
+ # A collection of compliance engine data controls
6
+ class ComplianceEngine::Controls < ComplianceEngine::Collection
7
+ private
8
+
9
+ # Returns the key of the collection in compliance engine source data
10
+ #
11
+ # @return [String]
12
+ def key
13
+ 'controls'
14
+ end
15
+
16
+ # Returns the class to use for the collection
17
+ #
18
+ # @return [Class]
19
+ def collected
20
+ ComplianceEngine::Control
21
+ end
22
+ end