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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +623 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +22 -0
- data/README.md +31 -0
- data/Rakefile +12 -0
- data/TODO.md +19 -0
- data/exe/compliance_engine +6 -0
- data/lib/compliance_engine/ce.rb +7 -0
- data/lib/compliance_engine/ces.rb +40 -0
- data/lib/compliance_engine/check.rb +38 -0
- data/lib/compliance_engine/checks.rb +22 -0
- data/lib/compliance_engine/cli.rb +76 -0
- data/lib/compliance_engine/collection.rb +132 -0
- data/lib/compliance_engine/component.rb +251 -0
- data/lib/compliance_engine/control.rb +7 -0
- data/lib/compliance_engine/controls.rb +22 -0
- data/lib/compliance_engine/data.rb +426 -0
- data/lib/compliance_engine/data_loader/file.rb +35 -0
- data/lib/compliance_engine/data_loader/json.rb +17 -0
- data/lib/compliance_engine/data_loader/yaml.rb +17 -0
- data/lib/compliance_engine/data_loader.rb +45 -0
- data/lib/compliance_engine/environment_loader/zip.rb +24 -0
- data/lib/compliance_engine/environment_loader.rb +31 -0
- data/lib/compliance_engine/module_loader.rb +62 -0
- data/lib/compliance_engine/profile.rb +13 -0
- data/lib/compliance_engine/profiles.rb +22 -0
- data/lib/compliance_engine/version.rb +24 -0
- data/lib/compliance_engine.rb +25 -0
- data/sig/compliance_engine.rbs +4 -0
- metadata +145 -0
@@ -0,0 +1,426 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'compliance_engine'
|
4
|
+
require 'compliance_engine/version'
|
5
|
+
require 'compliance_engine/component'
|
6
|
+
require 'compliance_engine/ce'
|
7
|
+
require 'compliance_engine/check'
|
8
|
+
require 'compliance_engine/control'
|
9
|
+
require 'compliance_engine/profile'
|
10
|
+
require 'compliance_engine/collection'
|
11
|
+
require 'compliance_engine/ces'
|
12
|
+
require 'compliance_engine/checks'
|
13
|
+
require 'compliance_engine/controls'
|
14
|
+
require 'compliance_engine/profiles'
|
15
|
+
|
16
|
+
require 'compliance_engine/data_loader'
|
17
|
+
require 'compliance_engine/data_loader/json'
|
18
|
+
require 'compliance_engine/data_loader/yaml'
|
19
|
+
require 'compliance_engine/module_loader'
|
20
|
+
require 'compliance_engine/environment_loader'
|
21
|
+
|
22
|
+
require 'deep_merge'
|
23
|
+
require 'json'
|
24
|
+
|
25
|
+
# Work with compliance data
|
26
|
+
class ComplianceEngine::Data
|
27
|
+
# @param [Array<String>] paths The paths to the compliance data files
|
28
|
+
# @param [Hash] facts The facts to use while evaluating the data
|
29
|
+
# @param [Integer] enforcement_tolerance The tolerance to use while evaluating the data
|
30
|
+
def initialize(*paths, facts: nil, enforcement_tolerance: nil)
|
31
|
+
@data ||= {}
|
32
|
+
@facts = facts
|
33
|
+
@enforcement_tolerance = enforcement_tolerance
|
34
|
+
open(*paths) unless paths.nil? || paths.empty?
|
35
|
+
end
|
36
|
+
|
37
|
+
# Setting any of these should all invalidate any cached data
|
38
|
+
attr_reader :data, :facts, :enforcement_tolerance, :environment_data, :modulepath
|
39
|
+
|
40
|
+
# Set the object data
|
41
|
+
# @param [Hash] data The data to initialize the object with
|
42
|
+
def data=(value)
|
43
|
+
@data = value
|
44
|
+
invalidate_cache
|
45
|
+
end
|
46
|
+
|
47
|
+
# Set the facts
|
48
|
+
# @param [Hash] facts The facts to initialize the object with
|
49
|
+
def facts=(value)
|
50
|
+
@facts = value
|
51
|
+
invalidate_cache
|
52
|
+
end
|
53
|
+
|
54
|
+
# Set the enforcement tolerance
|
55
|
+
# @param [Hash] enforcement_tolerance The enforcement tolerance to initialize
|
56
|
+
def enforcement_tolerance=(value)
|
57
|
+
@enforcement_tolerance = value
|
58
|
+
invalidate_cache
|
59
|
+
end
|
60
|
+
|
61
|
+
# Set the environment data
|
62
|
+
# @param [Hash] environment_data The environment data to initialize the object with
|
63
|
+
def environment_data=(value)
|
64
|
+
@environment_data = value
|
65
|
+
invalidate_cache
|
66
|
+
end
|
67
|
+
|
68
|
+
# Set the modulepath
|
69
|
+
# @param [Array<String>] modulepath The Puppet modulepath
|
70
|
+
def modulepath=(value)
|
71
|
+
@modulepath = value
|
72
|
+
invalidate_cache
|
73
|
+
end
|
74
|
+
|
75
|
+
# Invalidate the cache of computed data
|
76
|
+
#
|
77
|
+
# @return [NilClass]
|
78
|
+
def invalidate_cache
|
79
|
+
collection_variables.each { |var| instance_variable_get(var)&.invalidate_cache(self) }
|
80
|
+
cache_variables.each { |var| instance_variable_set(var, nil) }
|
81
|
+
end
|
82
|
+
|
83
|
+
# Discard all parsed data other than the top-level data
|
84
|
+
#
|
85
|
+
# @return [NilClass]
|
86
|
+
def reset_collection
|
87
|
+
# Discard any cached objects
|
88
|
+
(instance_variables - (data_variables + context_variables)).each { |var| instance_variable_set(var, nil) }
|
89
|
+
end
|
90
|
+
|
91
|
+
# Scan a Puppet environment from a zip file
|
92
|
+
# @param [String] path The Puppet environment archive file
|
93
|
+
# @return [NilClass]
|
94
|
+
def open_environment_zip(path)
|
95
|
+
require 'compliance_engine/environment_loader/zip'
|
96
|
+
|
97
|
+
environment = ComplianceEngine::EnvironmentLoader::Zip.new(path)
|
98
|
+
self.modulepath = environment.modulepath
|
99
|
+
open(environment)
|
100
|
+
end
|
101
|
+
|
102
|
+
# Scan a Puppet environment
|
103
|
+
# @param [Array<String>] paths The Puppet modulepath components
|
104
|
+
# @return [NilClass]
|
105
|
+
def open_environment(*paths)
|
106
|
+
environment = ComplianceEngine::EnvironmentLoader.new(*paths)
|
107
|
+
self.modulepath = environment.modulepath
|
108
|
+
open(environment)
|
109
|
+
end
|
110
|
+
|
111
|
+
# Scan paths for compliance data files
|
112
|
+
#
|
113
|
+
# @param [Array<String>] paths The paths to the compliance data files
|
114
|
+
# @param [Class] fileclass The class to use for reading files
|
115
|
+
# @param [Class] dirclass The class to use for reading directories
|
116
|
+
# @return [NilClass]
|
117
|
+
def open(*paths, fileclass: File, dirclass: Dir)
|
118
|
+
modules = {}
|
119
|
+
|
120
|
+
paths.each do |path|
|
121
|
+
if path.is_a?(ComplianceEngine::EnvironmentLoader)
|
122
|
+
open(*path.modules)
|
123
|
+
next
|
124
|
+
end
|
125
|
+
|
126
|
+
if path.is_a?(ComplianceEngine::ModuleLoader)
|
127
|
+
modules[path.name] = path.version unless path.name.nil?
|
128
|
+
path.files.each do |file_loader|
|
129
|
+
update(file_loader)
|
130
|
+
end
|
131
|
+
next
|
132
|
+
end
|
133
|
+
|
134
|
+
if path.is_a?(ComplianceEngine::DataLoader)
|
135
|
+
update(path, key: path.key, fileclass: fileclass)
|
136
|
+
next
|
137
|
+
end
|
138
|
+
|
139
|
+
if fileclass.file?(path)
|
140
|
+
key = if Object.const_defined?(:Zip) && path.is_a?(Zip::Entry)
|
141
|
+
File.join(path.zipfile.to_s, '.', path.to_s)
|
142
|
+
else
|
143
|
+
path.to_s
|
144
|
+
end
|
145
|
+
update(path, key: key, fileclass: fileclass)
|
146
|
+
next
|
147
|
+
end
|
148
|
+
|
149
|
+
if fileclass.directory?(path)
|
150
|
+
open(ComplianceEngine::ModuleLoader.new(path, fileclass: fileclass, dirclass: dirclass))
|
151
|
+
next
|
152
|
+
end
|
153
|
+
|
154
|
+
raise ComplianceEngine::Error, "Invalid path or object '#{path}'"
|
155
|
+
end
|
156
|
+
|
157
|
+
self.environment_data ||= {}
|
158
|
+
self.environment_data = self.environment_data.merge(modules)
|
159
|
+
|
160
|
+
nil
|
161
|
+
end
|
162
|
+
|
163
|
+
# Update the data for a given file
|
164
|
+
#
|
165
|
+
# @param [String] file The path to the compliance data file
|
166
|
+
# @param [String] key The key to use for the data
|
167
|
+
# @param [Class] fileclass The class to use for reading files
|
168
|
+
# @param [Integer] size The size of the file
|
169
|
+
# @param [Time] mtime The modification time of the file
|
170
|
+
# @return [NilClass]
|
171
|
+
def update(
|
172
|
+
filename,
|
173
|
+
key: filename.to_s,
|
174
|
+
fileclass: File
|
175
|
+
)
|
176
|
+
if filename.is_a?(String)
|
177
|
+
data[key] ||= {}
|
178
|
+
|
179
|
+
if data[key]&.key?(:loader) && data[key][:loader]
|
180
|
+
data[key][:loader].refresh if data[key][:loader].respond_to?(:refresh)
|
181
|
+
return
|
182
|
+
end
|
183
|
+
|
184
|
+
loader = if File.extname(filename) == '.json'
|
185
|
+
ComplianceEngine::DataLoader::Json.new(filename, fileclass: fileclass, key: key)
|
186
|
+
else
|
187
|
+
ComplianceEngine::DataLoader::Yaml.new(filename, fileclass: fileclass, key: key)
|
188
|
+
end
|
189
|
+
|
190
|
+
loader.add_observer(self, :update)
|
191
|
+
data[key] = {
|
192
|
+
loader: loader,
|
193
|
+
version: ComplianceEngine::Version.new(loader.data['version']),
|
194
|
+
content: loader.data,
|
195
|
+
}
|
196
|
+
else
|
197
|
+
data[filename.key] ||= {}
|
198
|
+
|
199
|
+
# Assume filename is a loader object
|
200
|
+
unless data[filename.key]&.key?(:loader)
|
201
|
+
data[filename.key][:loader] = filename
|
202
|
+
data[filename.key][:loader].add_observer(self, :update)
|
203
|
+
end
|
204
|
+
data[filename.key][:version] = ComplianceEngine::Version.new(filename.data['version'])
|
205
|
+
data[filename.key][:content] = filename.data
|
206
|
+
end
|
207
|
+
|
208
|
+
reset_collection
|
209
|
+
rescue => e
|
210
|
+
warn e.message
|
211
|
+
end
|
212
|
+
|
213
|
+
# Get a list of files with compliance data
|
214
|
+
#
|
215
|
+
# @return [Array<String>]
|
216
|
+
def files
|
217
|
+
return @files unless @files.nil?
|
218
|
+
@files = data.select { |_, file| file.key?(:content) }.keys
|
219
|
+
end
|
220
|
+
|
221
|
+
# Get the compliance data for a given file
|
222
|
+
#
|
223
|
+
# @param [String] file The path to the compliance data file
|
224
|
+
# @return [Hash]
|
225
|
+
def get(file)
|
226
|
+
data[file][:content]
|
227
|
+
rescue
|
228
|
+
nil
|
229
|
+
end
|
230
|
+
|
231
|
+
# Return a profile collection
|
232
|
+
#
|
233
|
+
# @return [ComplianceEngine::Profiles]
|
234
|
+
def profiles
|
235
|
+
@profiles ||= ComplianceEngine::Profiles.new(self)
|
236
|
+
end
|
237
|
+
|
238
|
+
# Return a collection of CEs
|
239
|
+
#
|
240
|
+
# @return [ComplianceEngine::CEs]
|
241
|
+
def ces
|
242
|
+
@ces ||= ComplianceEngine::Ces.new(self)
|
243
|
+
end
|
244
|
+
|
245
|
+
# Return a collection of checks
|
246
|
+
#
|
247
|
+
# @return [ComplianceEngine::Checks]
|
248
|
+
def checks
|
249
|
+
@checks ||= ComplianceEngine::Checks.new(self)
|
250
|
+
end
|
251
|
+
|
252
|
+
# Return a collection of controls
|
253
|
+
#
|
254
|
+
# @return [ComplianceEngine::Controls]
|
255
|
+
def controls
|
256
|
+
@controls ||= ComplianceEngine::Controls.new(self)
|
257
|
+
end
|
258
|
+
|
259
|
+
# Return all confines
|
260
|
+
#
|
261
|
+
# @return [Hash]
|
262
|
+
def confines
|
263
|
+
return @confines unless @confines.nil?
|
264
|
+
|
265
|
+
@confines ||= {}
|
266
|
+
|
267
|
+
[profiles, ces, checks, controls].each do |collection|
|
268
|
+
collection.each_value do |v|
|
269
|
+
v.to_a.each do |component|
|
270
|
+
next unless component.key?('confine')
|
271
|
+
@confines = @confines.deep_merge!(component['confine'])
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
@confines
|
277
|
+
end
|
278
|
+
|
279
|
+
# Return all Hiera data from checks that map to the requested profiles
|
280
|
+
#
|
281
|
+
# @param [Array<String>] requested_profiles The requested profiles
|
282
|
+
# @return [Hash]
|
283
|
+
def hiera(requested_profiles = [])
|
284
|
+
# If we have no valid profiles, we won't have any hiera data.
|
285
|
+
return {} if requested_profiles.empty?
|
286
|
+
|
287
|
+
cache_key = requested_profiles.to_s
|
288
|
+
|
289
|
+
@hiera ||= {}
|
290
|
+
|
291
|
+
return @hiera[cache_key] if @hiera.key?(cache_key)
|
292
|
+
|
293
|
+
valid_profiles = []
|
294
|
+
requested_profiles.each do |profile|
|
295
|
+
if profiles[profile].nil?
|
296
|
+
warn "Requested profile '#{profile}' not defined"
|
297
|
+
next
|
298
|
+
end
|
299
|
+
|
300
|
+
valid_profiles << profiles[profile]
|
301
|
+
end
|
302
|
+
|
303
|
+
# If we have no valid profiles, we won't have any hiera data.
|
304
|
+
if valid_profiles.empty?
|
305
|
+
@hiera[cache_key] = {}
|
306
|
+
return @hiera[cache_key]
|
307
|
+
end
|
308
|
+
|
309
|
+
parameters = {}
|
310
|
+
|
311
|
+
valid_profiles.reverse_each do |profile|
|
312
|
+
check_mapping(profile).each_value do |check|
|
313
|
+
parameters = parameters.deep_merge!(check.hiera)
|
314
|
+
end
|
315
|
+
end
|
316
|
+
|
317
|
+
@hiera[cache_key] = parameters
|
318
|
+
end
|
319
|
+
|
320
|
+
# Return all checks that map to the requested profile or CE
|
321
|
+
#
|
322
|
+
# @param [ComplianceEngine::Profile, ComplianceEngine::Ce] profile_or_ce The requested profile or CE
|
323
|
+
# @return [Hash]
|
324
|
+
def check_mapping(profile_or_ce)
|
325
|
+
raise ArgumentError, 'Argument must be a ComplianceEngine::Profile object' unless profile_or_ce.is_a?(ComplianceEngine::Profile) || profile_or_ce.is_a?(ComplianceEngine::Ce)
|
326
|
+
|
327
|
+
cache_key = "#{profile_or_ce.class}:#{profile_or_ce.key}"
|
328
|
+
|
329
|
+
@check_mapping ||= {}
|
330
|
+
|
331
|
+
return @check_mapping[cache_key] if @check_mapping.key?(cache_key)
|
332
|
+
|
333
|
+
@check_mapping[cache_key] = checks.select do |_, check|
|
334
|
+
mapping?(check, profile_or_ce)
|
335
|
+
end
|
336
|
+
end
|
337
|
+
|
338
|
+
private
|
339
|
+
|
340
|
+
# Get the collection variables
|
341
|
+
#
|
342
|
+
# @return [Array<Symbol>]
|
343
|
+
def collection_variables
|
344
|
+
[:@profiles, :@checks, :@controls, :@ces]
|
345
|
+
end
|
346
|
+
|
347
|
+
# Get the data variables
|
348
|
+
#
|
349
|
+
# @return [Array<Symbol>]
|
350
|
+
def data_variables
|
351
|
+
[:@data]
|
352
|
+
end
|
353
|
+
|
354
|
+
# Get the context variables
|
355
|
+
#
|
356
|
+
# @return [Array<Symbol>]
|
357
|
+
def context_variables
|
358
|
+
[:@enforcement_tolerance, :@environment_data, :@facts]
|
359
|
+
end
|
360
|
+
|
361
|
+
# Get the cache variables
|
362
|
+
#
|
363
|
+
# @return [Array<Symbol>]
|
364
|
+
def cache_variables
|
365
|
+
instance_variables - (data_variables + collection_variables + context_variables)
|
366
|
+
end
|
367
|
+
|
368
|
+
# Return true if the check is mapped to the profile or CE
|
369
|
+
#
|
370
|
+
# @param [ComplianceEngine::Check] check The check
|
371
|
+
# @param [ComplianceEngine::Profile, ComplianceEngine::Ce] profile_or_ce The profile or CE
|
372
|
+
# @return [TrueClass, FalseClass]
|
373
|
+
def mapping?(check, profile_or_ce)
|
374
|
+
raise ArgumentError, 'Argument must be a ComplianceEngine::Profile object' unless profile_or_ce.is_a?(ComplianceEngine::Profile) || profile_or_ce.is_a?(ComplianceEngine::Ce)
|
375
|
+
|
376
|
+
@mapping ||= {}
|
377
|
+
cache_key = [check.key, "#{profile_or_ce.class}:#{profile_or_ce.key}"].to_s
|
378
|
+
return @mapping[cache_key] if @mapping.key?(cache_key)
|
379
|
+
|
380
|
+
# Correlate based on controls
|
381
|
+
controls = check.controls&.select { |_, v| v }&.map { |k, _| k }
|
382
|
+
|
383
|
+
return @mapping[cache_key] = true if correlate(controls, profile_or_ce.controls)
|
384
|
+
|
385
|
+
if profile_or_ce.is_a?(ComplianceEngine::Ce)
|
386
|
+
# Correlate based on CEs
|
387
|
+
return @mapping[cache_key] = true if check.ces&.include?(profile_or_ce.key)
|
388
|
+
|
389
|
+
return @mapping[cache_key] = false
|
390
|
+
end
|
391
|
+
|
392
|
+
# Correlate based on direct reference to checks
|
393
|
+
return @mapping[cache_key] = true if profile_or_ce.checks&.dig(check.key)
|
394
|
+
|
395
|
+
# Correlate based on CEs
|
396
|
+
return @mapping[cache_key] = true if correlate(check.ces, profile_or_ce.ces)
|
397
|
+
|
398
|
+
# Correlate based on CEs and controls
|
399
|
+
return @mapping[cache_key] = true if profile_or_ce.ces&.any? { |k, _| correlate(controls, ces[k]&.controls) }
|
400
|
+
return @mapping[cache_key] = true if check.ces&.any? { |ce| ces[ce]&.controls&.any? { |k, v| v && profile_or_ce.controls&.dig(k) } }
|
401
|
+
|
402
|
+
@mapping[cache_key] = false
|
403
|
+
end
|
404
|
+
|
405
|
+
# Correlate between arrays and hashes
|
406
|
+
#
|
407
|
+
# @param [Array] a An array
|
408
|
+
# @param [Hash] b A hash
|
409
|
+
# @return [TrueClass, FalseClass]
|
410
|
+
def correlate(a, b)
|
411
|
+
return false if a.nil? || b.nil?
|
412
|
+
unless a.is_a?(Array) && b.is_a?(Hash)
|
413
|
+
raise ComplianceEngine::Error, "Expected array and hash, got #{a.class} and #{b.class}"
|
414
|
+
end
|
415
|
+
return false if a.empty? || b.empty?
|
416
|
+
|
417
|
+
a.any? { |item| b[item] }
|
418
|
+
end
|
419
|
+
|
420
|
+
# Print debugging messages to the console.
|
421
|
+
#
|
422
|
+
# @param [String] msg The message to print
|
423
|
+
def debug(msg)
|
424
|
+
warn msg
|
425
|
+
end
|
426
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'compliance_engine'
|
4
|
+
require 'compliance_engine/data_loader'
|
5
|
+
|
6
|
+
# Load compliance engine data from a file
|
7
|
+
class ComplianceEngine::DataLoader::File < ComplianceEngine::DataLoader
|
8
|
+
# Initialize a new instance of the ComplianceEngine::DataLoader::File class
|
9
|
+
#
|
10
|
+
# @param [String] file The path to the file to be loaded
|
11
|
+
# @param [Class] fileclass The class to use for file operations, defaults to `::File`
|
12
|
+
# @param [String] key The key to use for identifying the data, defaults to the file path
|
13
|
+
def initialize(file, fileclass: ::File, key: file)
|
14
|
+
@fileclass = fileclass
|
15
|
+
@filename = file
|
16
|
+
@size = fileclass.size(file)
|
17
|
+
@mtime = fileclass.mtime(file)
|
18
|
+
super(parse(fileclass.read(file)), key: key)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Refresh the data from the file if it has changed
|
22
|
+
#
|
23
|
+
# @return [NilClass]
|
24
|
+
def refresh
|
25
|
+
newsize = @fileclass.size(@filename)
|
26
|
+
newmtime = @fileclass.mtime(@filename)
|
27
|
+
return if newsize == @size && newmtime == @mtime
|
28
|
+
|
29
|
+
@size = newsize
|
30
|
+
@mtime = newmtime
|
31
|
+
self.data = parse(@fileclass.read(@filename))
|
32
|
+
end
|
33
|
+
|
34
|
+
attr_reader :key, :size, :mtime
|
35
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'compliance_engine'
|
4
|
+
require 'compliance_engine/data_loader/file'
|
5
|
+
|
6
|
+
require 'json'
|
7
|
+
|
8
|
+
# Load compliance engine data from a JSON file
|
9
|
+
class ComplianceEngine::DataLoader::Json < ComplianceEngine::DataLoader::File
|
10
|
+
# Parse JSON content into a Hash
|
11
|
+
#
|
12
|
+
# @param [String] content The content to parse
|
13
|
+
# @return [Hash]
|
14
|
+
def parse(content)
|
15
|
+
JSON.parse(content)
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'compliance_engine'
|
4
|
+
require 'compliance_engine/data_loader/file'
|
5
|
+
|
6
|
+
require 'yaml'
|
7
|
+
|
8
|
+
# Load compliance engine data from a YAML file
|
9
|
+
class ComplianceEngine::DataLoader::Yaml < ComplianceEngine::DataLoader::File
|
10
|
+
# Parse YAML content into a Hash
|
11
|
+
#
|
12
|
+
# @param [String] content The content to parse
|
13
|
+
# @return [Hash]
|
14
|
+
def parse(content)
|
15
|
+
YAML.safe_load(content)
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'compliance_engine'
|
4
|
+
require 'observer'
|
5
|
+
|
6
|
+
# Load compliance engine data
|
7
|
+
class ComplianceEngine::DataLoader
|
8
|
+
include Observable
|
9
|
+
|
10
|
+
# Initialize a new instance of the ComplianceEngine::DataLoader::File class
|
11
|
+
#
|
12
|
+
# @param value [Hash] The data to initialize the object with
|
13
|
+
# @param [String] key The key to use for identifying the data
|
14
|
+
def initialize(value = {}, key: nil)
|
15
|
+
self.data = value
|
16
|
+
@key = key
|
17
|
+
end
|
18
|
+
|
19
|
+
attr_reader :data
|
20
|
+
|
21
|
+
# Set the data for the data loader
|
22
|
+
#
|
23
|
+
# @param value [Hash] The new value for the data loader
|
24
|
+
#
|
25
|
+
# @raise [ComplianceEngine::Error] If the value is not a Hash
|
26
|
+
def data=(value)
|
27
|
+
raise ComplianceEngine::Error, 'Data must be a hash' unless value.is_a?(Hash)
|
28
|
+
@data = value
|
29
|
+
changed
|
30
|
+
notify_observers(self)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Get the key for the data loader
|
34
|
+
#
|
35
|
+
# The key is used to identify the data to observers. If a key is not
|
36
|
+
# provided during initialization, a random UUID will be generated.
|
37
|
+
#
|
38
|
+
# @return [String] The key for the data loader
|
39
|
+
def key
|
40
|
+
return @key unless @key.nil?
|
41
|
+
|
42
|
+
require 'securerandom'
|
43
|
+
@key = "#{data.class}:#{SecureRandom.uuid}"
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'compliance_engine'
|
4
|
+
require 'compliance_engine/environment_loader'
|
5
|
+
require 'zip/filesystem'
|
6
|
+
|
7
|
+
# Load compliance engine data from a zip file containing a Puppet environment
|
8
|
+
class ComplianceEngine::EnvironmentLoader::Zip < ComplianceEngine::EnvironmentLoader
|
9
|
+
# Initialize a ComplianceEngine::EnvironmentLoader::Zip object from a zip
|
10
|
+
# file and an optional root directory.
|
11
|
+
#
|
12
|
+
# @param path [String] the path to the zip file containing the Puppet environment
|
13
|
+
# @param root [String] a directory within the zip file to use as the root of the environment
|
14
|
+
def initialize(path, root: '/'.dup)
|
15
|
+
@modulepath = path
|
16
|
+
|
17
|
+
::Zip::File.open(path) do |zipfile|
|
18
|
+
dir = zipfile.dir
|
19
|
+
file = zipfile.file
|
20
|
+
|
21
|
+
super(path, root: root, fileclass: file, dirclass: dir)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'compliance_engine'
|
4
|
+
require 'compliance_engine/module_loader'
|
5
|
+
|
6
|
+
# Load compliance engine data from a Puppet environment
|
7
|
+
class ComplianceEngine::EnvironmentLoader
|
8
|
+
# Initialize an EnvironmentLoader from the components of a Puppet `modulepath`
|
9
|
+
#
|
10
|
+
# @param paths [Array] the paths to search for Puppet modules
|
11
|
+
# @param root [String] the root directory to search for Puppet modules
|
12
|
+
# @param fileclass [File] the class to use for file operations (default: `File`)
|
13
|
+
# @param dirclass [Dir] the class to use for directory operations (default: `Dir`)
|
14
|
+
def initialize(*paths, root: nil, fileclass: File, dirclass: Dir)
|
15
|
+
raise ArgumentError, 'No paths specified' if paths.empty?
|
16
|
+
@modulepath ||= paths
|
17
|
+
modules = paths.map do |path|
|
18
|
+
root ||= path
|
19
|
+
dirclass.entries(root)
|
20
|
+
.grep(%r{\A[a-z][a-z0-9_]*\Z})
|
21
|
+
.select { |child| fileclass.directory?(File.join(root, child)) }
|
22
|
+
.map { |child| File.join(root, child) }
|
23
|
+
rescue
|
24
|
+
[]
|
25
|
+
end
|
26
|
+
modules.flatten!
|
27
|
+
@modules = modules.map { |path| ComplianceEngine::ModuleLoader.new(path, fileclass: fileclass, dirclass: dirclass) }
|
28
|
+
end
|
29
|
+
|
30
|
+
attr_reader :modulepath, :modules
|
31
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'compliance_engine'
|
4
|
+
require 'compliance_engine/data_loader/json'
|
5
|
+
require 'compliance_engine/data_loader/yaml'
|
6
|
+
|
7
|
+
# Load compliance engine data from a Puppet module
|
8
|
+
class ComplianceEngine::ModuleLoader
|
9
|
+
# Initialize a ModuleLoader from a Puppet module path
|
10
|
+
#
|
11
|
+
# @param path [String] the path to the Puppet module
|
12
|
+
# @param fileclass [File] the class to use for file operations (default: `File`)
|
13
|
+
# @param dirclass [Dir] the class to use for directory operations (default: `Dir`)
|
14
|
+
def initialize(path, fileclass: File, dirclass: Dir)
|
15
|
+
raise ComplianceEngine::Error, "#{path} is not a directory" unless fileclass.directory?(path)
|
16
|
+
|
17
|
+
@name = nil
|
18
|
+
@version = nil
|
19
|
+
@files = []
|
20
|
+
|
21
|
+
# Read the Puppet module's metadata.json
|
22
|
+
metadata_json = File.join(path.to_s, 'metadata.json')
|
23
|
+
if fileclass.exist?(metadata_json)
|
24
|
+
begin
|
25
|
+
metadata = ComplianceEngine::DataLoader::Json.new(metadata_json, fileclass: fileclass)
|
26
|
+
@name = metadata.data['name']
|
27
|
+
@version = metadata.data['version']
|
28
|
+
rescue => e
|
29
|
+
warn "Could not parse #{metadata_json}: #{e.message}"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# In this directory, we want to look for all yaml and json files
|
34
|
+
# under SIMP/compliance_profiles and simp/compliance_profiles.
|
35
|
+
globs = ['SIMP/compliance_profiles', 'simp/compliance_profiles']
|
36
|
+
.select { |dir| fileclass.directory?(File.join(path, dir)) }
|
37
|
+
.map { |dir|
|
38
|
+
['yaml', 'json'].map { |type| File.join(path, dir, '**', "*.#{type}") }
|
39
|
+
}.flatten
|
40
|
+
# debug "Globs: #{globs}"
|
41
|
+
# Using .each here to make mocking with rspec easier.
|
42
|
+
globs.each do |glob|
|
43
|
+
dirclass.glob(glob).each do |file|
|
44
|
+
key = if Object.const_defined?(:Zip) && file.is_a?(Zip::Entry)
|
45
|
+
File.join(file.zipfile.to_s, '.', file.to_s)
|
46
|
+
else
|
47
|
+
file.to_s
|
48
|
+
end
|
49
|
+
loader = if File.extname(file.to_s) == '.json'
|
50
|
+
ComplianceEngine::DataLoader::Json.new(file.to_s, fileclass: fileclass, key: key)
|
51
|
+
else
|
52
|
+
ComplianceEngine::DataLoader::Yaml.new(file.to_s, fileclass: fileclass, key: key)
|
53
|
+
end
|
54
|
+
@files << loader
|
55
|
+
rescue => e
|
56
|
+
warn "Could not load #{file}: #{e.message}"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
attr_reader :name, :version, :files
|
62
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'compliance_engine'
|
4
|
+
|
5
|
+
# A compliance engine data profile
|
6
|
+
class ComplianceEngine::Profile < ComplianceEngine::Component
|
7
|
+
# Returns the checks of the profile
|
8
|
+
#
|
9
|
+
# @return [Hash] the checks of the profile
|
10
|
+
def checks
|
11
|
+
element['checks']
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'compliance_engine'
|
4
|
+
|
5
|
+
# A collection of compliance engine data profiles
|
6
|
+
class ComplianceEngine::Profiles < 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
|
+
'profiles'
|
14
|
+
end
|
15
|
+
|
16
|
+
# Returns the class to use for the collection
|
17
|
+
#
|
18
|
+
# @return [Class]
|
19
|
+
def collected
|
20
|
+
ComplianceEngine::Profile
|
21
|
+
end
|
22
|
+
end
|