compliance_engine 0.4.0 → 0.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f5129c30257114df1399ae314ff6a0fb5d383de5bd4782b8b31dc060ea274870
4
- data.tar.gz: 8dfaa1d2feaf1a43ea6551851eac5a586f2d4e2b4148efcd14cad21c32c93cba
3
+ metadata.gz: 739c21f5b10c86e4ec22f8fdbe5e86175a69f1bfd15af9d064a2fec3c6b11d7b
4
+ data.tar.gz: e142e74b1b28997ead592c6c358cc160cd094d307f162adc5f0edf77b3e22f68
5
5
  SHA512:
6
- metadata.gz: b4839e2648adfc6c1ea91d16c45a5494a6ad4da6521d816e16b9d0b3127b80d696d6f0cbb5249294cf6e0f21177809fbe9b8efd6c3665e3aae1bee6a02570134
7
- data.tar.gz: 68a0a04f93e3c007eec4287a92d6d19ace0ba37e9a739200474802ef28c6cb24b01257cec5e23f8b8e2cb8f29cf397641ac1e2fc5a1c117ca1db43f1932c2c3d
6
+ metadata.gz: f99f46c33175af5a2bc879ab5254fe4cc91334c11edf334c4809063063fd82279458de34e296d0af4dcc619cd9caa5342f3412f40aded8a73e629137716ea090
7
+ data.tar.gz: 0ff070e539cb02ef974de2b2c7de752a79b7e0e0e89f3306bd26780dabe91c836a347f12e8c6e1fe32c4fe0cd2b97c0eefd267ada51a6b22aff64cc5c8151f0f
data/CHANGELOG.md CHANGED
@@ -1,3 +1,13 @@
1
+ ### 0.5.0 / 2026-04-29
2
+ * Add JSON Schema for SCE data, exposed via `ComplianceEngine.schema` (#49)
3
+ * Add `ComplianceEngine::Tolerance` constants for enforcement tolerance levels (#107)
4
+ * Drop stale file data on environment rescan (#108)
5
+ * Handle malformed compliance data gracefully instead of raising (#111)
6
+ * Make dotfile loading opt-in (defaults to off) (#112)
7
+ * Accept `::Zip::File` objects directly in `EnvironmentLoader::Zip` (#114)
8
+ * Support `compliance_markup`-style parameter knockout prefixes
9
+ * Split `version.rb`: separate gem `VERSION` from data format validator (#104)
10
+
1
11
  ### 0.4.0 / 2026-04-20
2
12
  * Route Puppet Hiera backend log messages through Puppet's logging system (#96)
3
13
  * Fix JRuby compatibility: convert internal requires to require_relative so the library loads correctly inside Puppet Server / OpenVox Server when installed as a Puppet module rather than a standalone gem
@@ -19,7 +19,7 @@ Gem::Specification.new do |spec|
19
19
  spec.metadata['bug_tracker_uri'] = 'https://github.com/simp/rubygem-simp-compliance_engine/issues'
20
20
 
21
21
  # Specify which files should be added to the gem when it is released.
22
- spec.files = Dir.glob(['*.gemspec', '*.md', 'LICENSE', 'exe/*', 'lib/**/*.rb']).reject { |f| f.start_with?('lib/puppet/') }
22
+ spec.files = Dir.glob(['*.gemspec', '*.md', 'LICENSE', 'exe/*', 'lib/**/*.rb', 'lib/**/*.json']).reject { |f| f.start_with?('lib/puppet/') }
23
23
  spec.bindir = 'exe'
24
24
  spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
25
25
  spec.require_paths = ['lib']
@@ -12,7 +12,21 @@ class ComplianceEngine::Collection
12
12
  @collection ||= {}
13
13
  hash_key = key
14
14
  data.files.each do |file|
15
- data.get(file)[hash_key]&.each do |k, v|
15
+ file_data = data.get(file)
16
+ unless file_data.is_a?(Hash)
17
+ ComplianceEngine.log.debug "Skipping #{file}: expected Hash content, got #{file_data.class}"
18
+ next
19
+ end
20
+
21
+ entries = file_data[hash_key]
22
+ next if entries.nil?
23
+
24
+ unless entries.is_a?(Hash)
25
+ ComplianceEngine.log.error "Expected '#{hash_key}' in #{file} to be a Hash, got #{entries.class}"
26
+ next
27
+ end
28
+
29
+ entries.each do |k, v|
16
30
  @collection[k] ||= collected.new(k, data: self)
17
31
  @collection[k].add(file, v)
18
32
  end
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'set'
3
4
  require_relative '../compliance_engine'
4
- require_relative 'version'
5
+ require_relative 'data_version'
5
6
  require_relative 'component'
6
7
  require_relative 'ce'
7
8
  require_relative 'check'
@@ -161,9 +162,17 @@ class ComplianceEngine::Data
161
162
 
162
163
  if path.is_a?(ComplianceEngine::ModuleLoader)
163
164
  modules[path.name] = path.version unless path.name.nil?
164
- path.files.each do |file_loader|
165
- update(file_loader)
166
- end
165
+ new_keys = path.files.to_set(&:key)
166
+ module_root = if path.zipfile_path
167
+ ::File.join(path.zipfile_path, '.', path.path.sub(%r{^/+}, ''))
168
+ else
169
+ path.path
170
+ end
171
+ module_prefix = ::File.join(module_root, '')
172
+ stale_keys = data.keys.select { |k| (k == module_root || k.start_with?(module_prefix)) && !new_keys.include?(k) }
173
+ stale_keys.each { |k| data.delete(k) }
174
+ path.files.each { |file_loader| update(file_loader) }
175
+ reset_collection if path.files.empty? && !stale_keys.empty?
167
176
  next
168
177
  end
169
178
 
@@ -221,7 +230,7 @@ class ComplianceEngine::Data
221
230
  loader.add_observer(self, :update)
222
231
  data[key] = {
223
232
  loader: loader,
224
- version: ComplianceEngine::Version.new(loader.data['version']),
233
+ version: ComplianceEngine::DataVersion.new(loader.data['version']),
225
234
  content: loader.data,
226
235
  }
227
236
  else
@@ -237,7 +246,7 @@ class ComplianceEngine::Data
237
246
  data[filename.key][:loader] = filename
238
247
  data[filename.key][:loader].add_observer(self, :update)
239
248
  end
240
- data[filename.key][:version] = ComplianceEngine::Version.new(filename.data['version'])
249
+ data[filename.key][:version] = ComplianceEngine::DataVersion.new(filename.data['version'])
241
250
  data[filename.key][:content] = filename.data
242
251
  end
243
252
 
@@ -306,7 +315,8 @@ class ComplianceEngine::Data
306
315
  v.to_a.each do |component|
307
316
  next unless component.key?('confine')
308
317
 
309
- @confines = DeepMerge.deep_merge!(component['confine'], @confines)
318
+ confine = component['confine'].transform_values { |val| val.is_a?(Array) ? val.dup : Array(val) }
319
+ @confines = DeepMerge.deep_merge!(confine, @confines, knockout_prefix: '--')
310
320
  end
311
321
  end
312
322
  end
@@ -348,10 +358,24 @@ class ComplianceEngine::Data
348
358
 
349
359
  valid_profiles.reverse_each do |profile|
350
360
  check_mapping(profile).each_value do |check|
351
- parameters = DeepMerge.deep_merge!(check.hiera, parameters)
361
+ hiera_data = check.hiera
362
+ next if hiera_data.nil?
363
+
364
+ parameters = DeepMerge.deep_merge!(Marshal.load(Marshal.dump(hiera_data)), parameters)
352
365
  end
353
366
  end
354
367
 
368
+ # deep_merge does not support hash-key knockout via knockout_prefix.
369
+ # Handle parameter-name knockout explicitly: any key starting with '--'
370
+ # signals that the matching key without the prefix should be suppressed
371
+ # (mirrors compliance_markup behavior).
372
+ parameters.each_key do |key|
373
+ next unless key.start_with?('--')
374
+
375
+ parameters.delete(key.delete_prefix('--'))
376
+ parameters.delete(key)
377
+ end
378
+
355
379
  @hiera[cache_key] = parameters
356
380
  end
357
381
 
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../compliance_engine'
4
+
5
+ # Validates the version field found in compliance data files.
6
+ # Currently only version 2.0.0 of the data format is supported.
7
+ class ComplianceEngine::DataVersion
8
+ # @param version [String] the version string from a compliance data file
9
+ # @raise [ComplianceEngine::Error] if version is nil or not '2.0.0'
10
+ def initialize(version)
11
+ raise ComplianceEngine::Error, 'Missing version' if version.nil?
12
+ raise ComplianceEngine::Error, "Unsupported version '#{version}'" unless version == '2.0.0'
13
+
14
+ @version = version
15
+ end
16
+
17
+ # @return [String]
18
+ def to_s
19
+ @version
20
+ end
21
+ end
@@ -9,16 +9,21 @@ class ComplianceEngine::EnvironmentLoader::Zip < ComplianceEngine::EnvironmentLo
9
9
  # Initialize a ComplianceEngine::EnvironmentLoader::Zip object from a zip
10
10
  # file and an optional root directory.
11
11
  #
12
- # @param path [String] the path to the zip file containing the Puppet environment
12
+ # @param input [String, ::Zip::File] either a filesystem path to a zip file,
13
+ # or an already-opened ::Zip::File (e.g. from Zip::File.open_buffer); when
14
+ # a ::Zip::File is passed, the caller owns its lifecycle
13
15
  # @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(root, fileclass: file, dirclass: dir, zipfile_path: path)
22
- end
16
+ # @param load_dotfiles [Boolean] whether to load dotfiles; defaults to true to
17
+ # preserve the historical zip-loader behaviour of including all files
18
+ # @param name [String, nil] identifier used for modulepath and downstream
19
+ # cache keys; defaults to the zip's #name (the path on disk, or "-" for
20
+ # buffer-opened zips). Pass an explicit value when loading a buffer-opened
21
+ # zip to keep cache keys unique and logs informative.
22
+ def initialize(input, root: '/'.dup, load_dotfiles: true, name: nil)
23
+ zipfile = input.is_a?(::Zip::File) ? input : ::Zip::File.open(input)
24
+ @modulepath = name || zipfile.name
25
+ super(root, fileclass: zipfile.file, dirclass: zipfile.dir, zipfile_path: @modulepath, load_dotfiles: load_dotfiles)
26
+ ensure
27
+ zipfile.close if zipfile && !input.is_a?(::Zip::File)
23
28
  end
24
29
  end
@@ -11,7 +11,9 @@ class ComplianceEngine::EnvironmentLoader
11
11
  # @param fileclass [File] the class to use for file operations (default: `File`)
12
12
  # @param dirclass [Dir] the class to use for directory operations (default: `Dir`)
13
13
  # @param zipfile_path [String, nil] the path to the zip file if loading from a zip archive
14
- def initialize(*paths, fileclass: File, dirclass: Dir, zipfile_path: nil)
14
+ # @param load_dotfiles [Boolean] whether to load dotfiles; passed through to
15
+ # each ModuleLoader (default: false)
16
+ def initialize(*paths, fileclass: File, dirclass: Dir, zipfile_path: nil, load_dotfiles: false)
15
17
  raise ArgumentError, 'No paths specified' if paths.empty?
16
18
 
17
19
  @modulepath ||= paths
@@ -26,7 +28,7 @@ class ComplianceEngine::EnvironmentLoader
26
28
  []
27
29
  end
28
30
  modules.flatten!
29
- @modules = modules.map { |path| ComplianceEngine::ModuleLoader.new(path, fileclass: fileclass, dirclass: dirclass, zipfile_path: @zipfile_path) }
31
+ @modules = modules.map { |path| ComplianceEngine::ModuleLoader.new(path, fileclass: fileclass, dirclass: dirclass, zipfile_path: @zipfile_path, load_dotfiles: load_dotfiles) }
30
32
  end
31
33
 
32
34
  attr_reader :modulepath, :modules, :zipfile_path
@@ -12,12 +12,18 @@ class ComplianceEngine::ModuleLoader
12
12
  # @param fileclass [File] the class to use for file operations (default: `File`)
13
13
  # @param dirclass [Dir] the class to use for directory operations (default: `Dir`)
14
14
  # @param zipfile_path [String, nil] the path to the zip file if loading from a zip archive
15
- def initialize(path, fileclass: File, dirclass: Dir, zipfile_path: nil)
15
+ # @param load_dotfiles [Boolean] whether to load files whose relative path contains
16
+ # a component (directory or filename) beginning with '.'. Defaults to false so that
17
+ # dotfiles are skipped during normal module scanning, matching the behavior of
18
+ # Ruby's Dir.glob on real filesystems. Set to true only when the caller explicitly
19
+ # needs dotfile support (e.g. zip-based environment loading).
20
+ def initialize(path, fileclass: File, dirclass: Dir, zipfile_path: nil, load_dotfiles: false)
16
21
  raise ComplianceEngine::Error, "#{path} is not a directory" unless fileclass.directory?(path)
17
22
 
18
23
  @name = nil
19
24
  @version = nil
20
25
  @files = []
26
+ @path = path.to_s
21
27
  @zipfile_path = zipfile_path
22
28
 
23
29
  # Read the Puppet module's metadata.json
@@ -34,30 +40,42 @@ class ComplianceEngine::ModuleLoader
34
40
 
35
41
  # In this directory, we want to look for all yaml and json files
36
42
  # under SIMP/compliance_profiles and simp/compliance_profiles.
37
- globs = ['SIMP/compliance_profiles', 'simp/compliance_profiles']
38
- .select { |dir| fileclass.directory?(File.join(path, dir)) }
39
- .map { |dir|
40
- ['yaml', 'json'].map { |type| File.join(path, dir, '**', "*.#{type}") }
41
- }.flatten
42
- # Using .each here to make mocking with rspec easier.
43
- globs.each do |glob|
44
- dirclass.glob(glob).sort.each do |file|
45
- key = if @zipfile_path
46
- File.join(@zipfile_path, '.', file.to_s)
47
- else
48
- file.to_s
49
- end
50
- loader = if File.extname(file.to_s) == '.json'
51
- ComplianceEngine::DataLoader::Json.new(file.to_s, fileclass: fileclass, key: key)
52
- else
53
- ComplianceEngine::DataLoader::Yaml.new(file.to_s, fileclass: fileclass, key: key)
54
- end
55
- @files << loader
56
- rescue StandardError => e
57
- ComplianceEngine.log.warn "Could not load #{file}: #{e.message}"
43
+ # The loops are structured this way (rather than building a flat globs
44
+ # array first) so that each glob result can be checked against its
45
+ # base directory for dotfile filtering.
46
+ ['SIMP/compliance_profiles', 'simp/compliance_profiles'].each do |dir|
47
+ base = File.join(path, dir)
48
+ next unless fileclass.directory?(base)
49
+
50
+ # Using .each here to make mocking with rspec easier.
51
+ ['yaml', 'json'].each do |type|
52
+ dirclass.glob(File.join(base, '**', "*.#{type}")).sort.each do |file|
53
+ unless load_dotfiles
54
+ # Skip any file whose path (relative to the compliance_profiles
55
+ # base) contains a component beginning with '.', e.g. hidden
56
+ # files (.profile.yaml) or files inside hidden directories
57
+ # (.hidden/profile.yaml).
58
+ relative = file.to_s.delete_prefix("#{base}/")
59
+ next if relative.split('/').any? { |part| part.start_with?('.') }
60
+ end
61
+
62
+ key = if @zipfile_path
63
+ File.join(@zipfile_path, '.', file.to_s)
64
+ else
65
+ file.to_s
66
+ end
67
+ loader = if File.extname(file.to_s) == '.json'
68
+ ComplianceEngine::DataLoader::Json.new(file.to_s, fileclass: fileclass, key: key)
69
+ else
70
+ ComplianceEngine::DataLoader::Yaml.new(file.to_s, fileclass: fileclass, key: key)
71
+ end
72
+ @files << loader
73
+ rescue StandardError => e
74
+ ComplianceEngine.log.warn "Could not load #{file}: #{e.message}"
75
+ end
58
76
  end
59
77
  end
60
78
  end
61
79
 
62
- attr_reader :name, :version, :files, :zipfile_path
80
+ attr_reader :name, :version, :files, :path, :zipfile_path
63
81
  end
@@ -0,0 +1,517 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://simp-project.com/docs/sce/schema.json",
4
+ "type": "object",
5
+ "default": {},
6
+ "title": "Compliance Engine data",
7
+ "description": "Data for the Sicura Compliance Engine",
8
+ "required": [
9
+ "version"
10
+ ],
11
+ "additionalProperties": false,
12
+ "$defs": {
13
+ "confine": {
14
+ "type": "object",
15
+ "title": "Conditions that confine this entry to specific environments",
16
+ "description": "Keys are dot-notation Puppet fact paths (e.g. os.release.major) or the special keys module_name and module_version. Values may be a single scalar (exact match; strings may be prefixed with ! to negate) or an array of scalars (any match).",
17
+ "patternProperties": {
18
+ "^\\w+(?:[.\\-]\\w+)*$": {
19
+ "oneOf": [
20
+ {
21
+ "type": "string"
22
+ },
23
+ {
24
+ "type": "boolean"
25
+ },
26
+ {
27
+ "type": "integer"
28
+ },
29
+ {
30
+ "type": "array",
31
+ "items": {
32
+ "type": ["string", "boolean", "integer"]
33
+ }
34
+ }
35
+ ]
36
+ }
37
+ },
38
+ "examples": [
39
+ {
40
+ "os.release.major": ["8"],
41
+ "os.name": "RedHat"
42
+ },
43
+ {
44
+ "ipv6_enabled": true
45
+ },
46
+ {
47
+ "module_name": "simp-compliance_engine",
48
+ "module_version": ">=1.0.0 <2.0.0"
49
+ }
50
+ ]
51
+ },
52
+ "controlsMap": {
53
+ "type": "object",
54
+ "title": "References to top-level controls entries. Enforced when value is true.",
55
+ "patternProperties": {
56
+ "^\\w(?:[\\w.-]*[\\w-])?(?::\\w(?:[\\w.-]*[\\w-])?)*$": {
57
+ "type": "boolean",
58
+ "default": false
59
+ }
60
+ },
61
+ "additionalProperties": false,
62
+ "examples": [
63
+ {
64
+ "nist_800_53:rev4:AU-2": true
65
+ }
66
+ ]
67
+ },
68
+ "identifiers": {
69
+ "title": "External framework identifiers",
70
+ "description": "Preferred form: an object whose keys are framework names (e.g. nist_800_53:rev4, disa_stig) and whose values are arrays of identifier strings or numbers within that framework. A plain array of identifier strings is also accepted.",
71
+ "oneOf": [
72
+ {
73
+ "type": "object",
74
+ "patternProperties": {
75
+ "^\\w(?:[\\w.-]*[\\w-])?(?::\\w(?:[\\w.-]*[\\w-])?)*$": {
76
+ "type": "array",
77
+ "items": {
78
+ "type": ["string", "number"]
79
+ }
80
+ }
81
+ },
82
+ "examples": [
83
+ {
84
+ "nist_800_53:rev4": ["AU-2", "AU-3"],
85
+ "disa_stig": ["RHEL-07-021820", "CCI-000293"]
86
+ }
87
+ ]
88
+ },
89
+ {
90
+ "type": "array",
91
+ "items": {
92
+ "type": ["string", "number"]
93
+ },
94
+ "examples": [
95
+ ["ID1.1", "ID1.2"]
96
+ ]
97
+ }
98
+ ]
99
+ }
100
+ },
101
+ "properties": {
102
+ "version": {
103
+ "type": "string",
104
+ "const": "2.0.0",
105
+ "title": "The SCE data format version"
106
+ },
107
+ "profiles": {
108
+ "type": "object",
109
+ "default": {},
110
+ "title": "Collection of compliance profiles",
111
+ "description": "Each profile is a named checklist of Checks, CEs, and Controls to enforce.",
112
+ "patternProperties": {
113
+ "^\\w(?:[\\w.-]*[\\w-])?(?::\\w(?:[\\w.-]*[\\w-])?)*$": {
114
+ "type": "object",
115
+ "default": {},
116
+ "title": "A compliance profile",
117
+ "properties": {
118
+ "title": {
119
+ "type": "string",
120
+ "title": "Short description of the profile",
121
+ "examples": [
122
+ "Level 1 - Server"
123
+ ]
124
+ },
125
+ "description": {
126
+ "type": "string",
127
+ "title": "Longer description of the profile",
128
+ "examples": [
129
+ "Items in this profile intend to: be practical and prudent; provide a clear security benefit; and not inhibit the utility of the technology beyond acceptable means. This profile is intended for servers."
130
+ ]
131
+ },
132
+ "ces": {
133
+ "type": "object",
134
+ "default": {},
135
+ "title": "CEs to include in this profile. Enforced when value is true.",
136
+ "patternProperties": {
137
+ "^\\w(?:[\\w.-]*[\\w-])?(?::\\w(?:[\\w.-]*[\\w-])?)*$": {
138
+ "type": "boolean",
139
+ "default": false
140
+ }
141
+ },
142
+ "examples": [
143
+ {
144
+ "oval:simp.cis.el8.1.6.2_Ensure_ASLR_is_enabled:def:1": true
145
+ }
146
+ ]
147
+ },
148
+ "controls": {
149
+ "$ref": "#/$defs/controlsMap"
150
+ },
151
+ "checks": {
152
+ "type": "object",
153
+ "default": {},
154
+ "title": "Checks to include in this profile. Enforced when value is true.",
155
+ "patternProperties": {
156
+ "^\\w(?:[\\w.-]*[\\w-])?(?::\\w(?:[\\w.-]*[\\w-])?)*$": {
157
+ "type": "boolean",
158
+ "default": false
159
+ }
160
+ },
161
+ "examples": [
162
+ {
163
+ "widget_spinner_audit_logging": true
164
+ }
165
+ ]
166
+ },
167
+ "confine": {
168
+ "$ref": "#/$defs/confine"
169
+ }
170
+ },
171
+ "examples": [
172
+ {
173
+ "title": "Level 1 - Server",
174
+ "description": "Items in this profile intend to: be practical and prudent; provide a clear security benefit; and not inhibit the utility of the technology beyond acceptable means. This profile is intended for servers.",
175
+ "controls": {
176
+ "cis:el8:l1": true
177
+ }
178
+ }
179
+ ]
180
+ }
181
+ },
182
+ "examples": [
183
+ {
184
+ "cis:el8:l1:server": {
185
+ "title": "CIS Level 1 - Server",
186
+ "controls": {
187
+ "cis:el8:l1": true
188
+ }
189
+ }
190
+ }
191
+ ]
192
+ },
193
+ "ce": {
194
+ "type": "object",
195
+ "default": {},
196
+ "title": "Collection of Compliance Elements (CEs)",
197
+ "description": "Each CE names a single compliance capability and bridges profiles to checks via a shared vocabulary.",
198
+ "patternProperties": {
199
+ "^\\w(?:[\\w.-]*[\\w-])?(?::\\w(?:[\\w.-]*[\\w-])?)*$": {
200
+ "type": "object",
201
+ "default": {},
202
+ "title": "A Compliance Element",
203
+ "properties": {
204
+ "title": {
205
+ "type": "string",
206
+ "title": "Short title of the compliance element",
207
+ "examples": [
208
+ "Ensure address space layout randomization (ASLR) is enabled"
209
+ ]
210
+ },
211
+ "description": {
212
+ "type": "string",
213
+ "title": "Detailed description of the compliance element",
214
+ "examples": [
215
+ "Address space layout randomization (ASLR) is an exploit mitigation technique which randomly arranges the address space of key data areas of a process."
216
+ ]
217
+ },
218
+ "controls": {
219
+ "$ref": "#/$defs/controlsMap"
220
+ },
221
+ "identifiers": {
222
+ "$ref": "#/$defs/identifiers"
223
+ },
224
+ "oval-ids": {
225
+ "type": "array",
226
+ "default": [],
227
+ "title": "OVAL identifiers associated with this CE",
228
+ "items": {
229
+ "type": "string"
230
+ },
231
+ "examples": [
232
+ [
233
+ "xccdf_org.cisecurity.benchmarks_rule_1.6.2_Ensure_ASLR_is_enabled"
234
+ ]
235
+ ]
236
+ },
237
+ "imported_data": {
238
+ "type": "object",
239
+ "default": {},
240
+ "title": "Data imported from external benchmark sources",
241
+ "properties": {
242
+ "fixtext": {
243
+ "type": "string",
244
+ "title": "Remediation instructions from the source benchmark"
245
+ }
246
+ },
247
+ "additionalProperties": true
248
+ },
249
+ "confine": {
250
+ "$ref": "#/$defs/confine"
251
+ }
252
+ },
253
+ "examples": [
254
+ {
255
+ "controls": {
256
+ "cis:el8:v1.0.0.1": true
257
+ },
258
+ "identifiers": {
259
+ "cis": ["1.6.2"],
260
+ "nist_800_53:rev4": ["SC-39"]
261
+ },
262
+ "title": "Ensure address space layout randomization (ASLR) is enabled",
263
+ "description": "Address space layout randomization (ASLR) is an exploit mitigation technique which randomly arranges the address space of key data areas of a process.",
264
+ "confine": {
265
+ "os.release.major": ["8"],
266
+ "os.name": ["RedHat"]
267
+ }
268
+ }
269
+ ]
270
+ }
271
+ }
272
+ },
273
+ "checks": {
274
+ "type": "object",
275
+ "default": {},
276
+ "title": "Collection of compliance checks",
277
+ "description": "Each check is a verifiable assertion about a system setting. Currently only type puppet-class-parameter is supported; it carries a parameter and value that become Hiera data.",
278
+ "patternProperties": {
279
+ "^\\w(?:[\\w.-]*[\\w-])?(?::\\w(?:[\\w.-]*[\\w-])?)*$": {
280
+ "type": "object",
281
+ "default": {},
282
+ "title": "A compliance check",
283
+ "properties": {
284
+ "type": {
285
+ "type": "string",
286
+ "const": "puppet-class-parameter",
287
+ "title": "The type of check"
288
+ },
289
+ "settings": {
290
+ "type": "object",
291
+ "title": "Settings for the check",
292
+ "required": ["parameter", "value"],
293
+ "properties": {
294
+ "parameter": {
295
+ "type": "string",
296
+ "title": "The fully-qualified Puppet class parameter name. Prefix with -- to knock out an existing parameter.",
297
+ "examples": [
298
+ "simp::sysctl::kernel__randomize_va_space",
299
+ "--simp::sysctl::kernel__randomize_va_space"
300
+ ]
301
+ },
302
+ "value": {
303
+ "title": "The value to enforce for the parameter",
304
+ "type": ["boolean", "integer", "number", "string", "array", "object", "null"],
305
+ "examples": [
306
+ 2,
307
+ true,
308
+ "weekly",
309
+ ["item1", "item2"]
310
+ ]
311
+ }
312
+ },
313
+ "examples": [
314
+ {
315
+ "parameter": "simp::sysctl::kernel__randomize_va_space",
316
+ "value": 2
317
+ }
318
+ ]
319
+ },
320
+ "ces": {
321
+ "type": "array",
322
+ "default": [],
323
+ "title": "CEs that this check satisfies",
324
+ "items": {
325
+ "type": "string"
326
+ },
327
+ "examples": [
328
+ [
329
+ "oval:simp.cis.el8.1.6.2_Ensure_ASLR_is_enabled:def:1"
330
+ ]
331
+ ]
332
+ },
333
+ "controls": {
334
+ "$ref": "#/$defs/controlsMap"
335
+ },
336
+ "identifiers": {
337
+ "$ref": "#/$defs/identifiers"
338
+ },
339
+ "confine": {
340
+ "$ref": "#/$defs/confine"
341
+ },
342
+ "remediation": {
343
+ "type": "object",
344
+ "title": "Remediation metadata used to filter checks by enforcement tolerance",
345
+ "properties": {
346
+ "risk": {
347
+ "type": "array",
348
+ "title": "Risk entries describing the potential impact of enforcing this check",
349
+ "items": {
350
+ "type": "object",
351
+ "properties": {
352
+ "level": {
353
+ "type": "integer",
354
+ "minimum": 1,
355
+ "maximum": 100,
356
+ "title": "Numeric risk level (1-100). Checks are skipped when this value is >= the enforcement_tolerance setting."
357
+ },
358
+ "reason": {
359
+ "type": "string",
360
+ "title": "Human-readable explanation of the risk"
361
+ }
362
+ }
363
+ },
364
+ "examples": [
365
+ [
366
+ {
367
+ "level": 41,
368
+ "reason": "Depending on the system being scanned, aide could cause system sluggishness during the period it performs its scan."
369
+ }
370
+ ]
371
+ ]
372
+ },
373
+ "disabled": {
374
+ "type": "array",
375
+ "title": "Entries indicating this check is disabled. Disabled checks are always skipped when enforcement_tolerance is set.",
376
+ "items": {
377
+ "type": "object",
378
+ "properties": {
379
+ "reason": {
380
+ "type": "string",
381
+ "title": "Human-readable explanation of why this check is disabled"
382
+ }
383
+ }
384
+ },
385
+ "examples": [
386
+ [
387
+ {
388
+ "reason": "This check is not applicable to containerized environments."
389
+ }
390
+ ]
391
+ ]
392
+ }
393
+ }
394
+ }
395
+ },
396
+ "if": {
397
+ "required": ["type"]
398
+ },
399
+ "then": {
400
+ "required": ["settings"]
401
+ },
402
+ "examples": [
403
+ {
404
+ "type": "puppet-class-parameter",
405
+ "settings": {
406
+ "parameter": "simp::sysctl::kernel__randomize_va_space",
407
+ "value": 2
408
+ },
409
+ "controls": {
410
+ "nist_800_53:rev4:SC-39": true
411
+ },
412
+ "confine": {
413
+ "os.family": "RedHat",
414
+ "os.release.major": ["7", "8", "9"]
415
+ }
416
+ }
417
+ ]
418
+ }
419
+ }
420
+ },
421
+ "controls": {
422
+ "type": "object",
423
+ "default": {},
424
+ "title": "Collection of compliance controls",
425
+ "description": "Controls are cross-reference labels from external frameworks (e.g. nist_800_53:rev4:AU-2, disa_stig). Profiles and checks annotate themselves with controls to express alignment. Control entries carry optional metadata; an empty object {} is valid.",
426
+ "patternProperties": {
427
+ "^\\w(?:[\\w.-]*[\\w-])?(?::\\w(?:[\\w.-]*[\\w-])?)*$": {
428
+ "type": "object",
429
+ "default": {},
430
+ "title": "A compliance control",
431
+ "properties": {
432
+ "title": {
433
+ "type": "string",
434
+ "title": "Short title of the control"
435
+ },
436
+ "description": {
437
+ "type": "string",
438
+ "title": "Description of the control"
439
+ },
440
+ "identifiers": {
441
+ "$ref": "#/$defs/identifiers"
442
+ },
443
+ "confine": {
444
+ "$ref": "#/$defs/confine"
445
+ }
446
+ },
447
+ "examples": [
448
+ {},
449
+ {
450
+ "title": "Audit Events",
451
+ "identifiers": {
452
+ "nist_800_53:rev4": ["AU-2"]
453
+ }
454
+ }
455
+ ]
456
+ }
457
+ },
458
+ "examples": [
459
+ {
460
+ "nist_800_53:rev4": {},
461
+ "disa_stig": {},
462
+ "nist_800_53:rev4:AU-2": {
463
+ "title": "Audit Events"
464
+ }
465
+ }
466
+ ]
467
+ }
468
+ },
469
+ "examples": [
470
+ {
471
+ "version": "2.0.0",
472
+ "profiles": {
473
+ "cis:el8:l1:server": {
474
+ "title": "CIS Level 1 - Server",
475
+ "controls": {
476
+ "cis:el8:l1": true
477
+ }
478
+ }
479
+ },
480
+ "ce": {
481
+ "oval:simp.cis.el8.1.6.2_Ensure_ASLR_is_enabled:def:1": {
482
+ "controls": {
483
+ "cis:el8:l1": true
484
+ },
485
+ "identifiers": {
486
+ "cis": ["1.6.2"],
487
+ "nist_800_53:rev4": ["SC-39"]
488
+ },
489
+ "title": "Ensure address space layout randomization (ASLR) is enabled",
490
+ "description": "ASLR is an exploit mitigation technique which randomly arranges the address space of key data areas of a process.",
491
+ "confine": {
492
+ "os.release.major": ["8"],
493
+ "os.name": ["RedHat"]
494
+ }
495
+ }
496
+ },
497
+ "checks": {
498
+ "oval:com.puppet.forge.simp.cis.simp.sysctl.kernel__randomize_va_space": {
499
+ "type": "puppet-class-parameter",
500
+ "settings": {
501
+ "parameter": "simp::sysctl::kernel__randomize_va_space",
502
+ "value": 2
503
+ },
504
+ "controls": {
505
+ "cis:el8:l1": true
506
+ },
507
+ "confine": {
508
+ "os.family": "RedHat"
509
+ }
510
+ }
511
+ },
512
+ "controls": {
513
+ "cis:el8:l1": {}
514
+ }
515
+ }
516
+ ]
517
+ }
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ComplianceEngine
4
+ # Named constants for enforcement tolerance levels.
5
+ #
6
+ # The enforcement tolerance controls which remediation risk levels are
7
+ # enforced. A check whose risk level is greater than or equal to the
8
+ # tolerance value is filtered out. For example, setting
9
+ # +enforcement_tolerance+ to +ComplianceEngine::Tolerance::MODERATE+
10
+ # enforces checks with risk levels below 60 while skipping anything
11
+ # rated MODERATE (60) or higher.
12
+ module Tolerance
13
+ # Enforce only checks with no meaningful risk (risk < 20).
14
+ NONE = 20
15
+
16
+ # Enforce checks below low-risk remediations; skip SAFE (40) and above (risk < 40).
17
+ SAFE = 40
18
+
19
+ # Enforce checks below moderate-risk remediations; skip MODERATE (60) and above (risk < 60).
20
+ MODERATE = 60
21
+
22
+ # Enforce checks below remediations that affect access; skip ACCESS (80) and above (risk < 80).
23
+ ACCESS = 80
24
+
25
+ # Enforce all checks, including those that may cause breaking changes (risk < 100).
26
+ BREAKING = 100
27
+ end
28
+ end
@@ -1,25 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ComplianceEngine
4
- VERSION = '0.4.0'
5
-
6
- # Handle supported compliance data versions
7
- class Version
8
- # Verify that the version is supported
9
- #
10
- # @param version [String] The version to verify
11
- def initialize(version)
12
- raise 'Missing version' if version.nil?
13
- raise "Unsupported version '#{version}'" unless version == '2.0.0'
14
-
15
- @version = version
16
- end
17
-
18
- # Convert the version to a string
19
- #
20
- # @return [String]
21
- def to_s
22
- @version
23
- end
24
- end
4
+ VERSION = '0.5.0'
25
5
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'compliance_engine/version'
4
+ require_relative 'compliance_engine/tolerance'
4
5
  require_relative 'compliance_engine/data'
5
6
  require 'logger'
6
7
 
@@ -42,6 +43,44 @@ module ComplianceEngine
42
43
  @log = value
43
44
  end
44
45
 
46
+ # Return the path to the bundled JSON schema for SCE data files
47
+ #
48
+ # @return [String] absolute path to sce-schema.json
49
+ def self.schema_path
50
+ File.expand_path(File.join(__dir__, 'compliance_engine', 'sce-schema.json'))
51
+ end
52
+
53
+ # Return the parsed JSON schema for SCE data files
54
+ #
55
+ # @return [Hash] the parsed JSON schema
56
+ def self.schema
57
+ require 'json'
58
+ @schema ||= begin
59
+ path = schema_path
60
+ deep_freeze(JSON.parse(File.read(path, encoding: 'UTF-8')))
61
+ rescue Errno::ENOENT, JSON::ParserError => e
62
+ raise Error, "Failed to load schema from #{path}: #{e.class}: #{e.message}"
63
+ end
64
+ end
65
+
66
+ # Recursively freeze a Hash, Array, and all nested objects.
67
+ #
68
+ # @param obj [Object] the object to freeze
69
+ # @return [Object] the frozen object
70
+ def self.deep_freeze(obj)
71
+ case obj
72
+ when Hash
73
+ obj.each do |k, v|
74
+ deep_freeze(k)
75
+ deep_freeze(v)
76
+ end
77
+ when Array
78
+ obj.each { |v| deep_freeze(v) }
79
+ end
80
+ obj.freeze
81
+ end
82
+ private_class_method :deep_freeze
83
+
45
84
  # Install a PuppetLogger unless a logger has already been explicitly configured.
46
85
  # Extracted so the behaviour can be unit-tested without reloading enforcement.rb.
47
86
  #
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: compliance_engine
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Steven Pritchard
@@ -142,12 +142,15 @@ files:
142
142
  - lib/compliance_engine/data_loader/file.rb
143
143
  - lib/compliance_engine/data_loader/json.rb
144
144
  - lib/compliance_engine/data_loader/yaml.rb
145
+ - lib/compliance_engine/data_version.rb
145
146
  - lib/compliance_engine/environment_loader.rb
146
147
  - lib/compliance_engine/environment_loader/zip.rb
147
148
  - lib/compliance_engine/module_loader.rb
148
149
  - lib/compliance_engine/profile.rb
149
150
  - lib/compliance_engine/profiles.rb
150
151
  - lib/compliance_engine/puppet_logger.rb
152
+ - lib/compliance_engine/sce-schema.json
153
+ - lib/compliance_engine/tolerance.rb
151
154
  - lib/compliance_engine/version.rb
152
155
  homepage: https://simp-project.com/docs/sce/
153
156
  licenses:
@@ -155,7 +158,7 @@ licenses:
155
158
  metadata:
156
159
  homepage_uri: https://simp-project.com/docs/sce/
157
160
  source_code_uri: https://github.com/simp/rubygem-simp-compliance_engine
158
- changelog_uri: https://github.com/simp/rubygem-simp-compliance_engine/releases/tag/0.4.0
161
+ changelog_uri: https://github.com/simp/rubygem-simp-compliance_engine/releases/tag/0.5.0
159
162
  bug_tracker_uri: https://github.com/simp/rubygem-simp-compliance_engine/issues
160
163
  rdoc_options: []
161
164
  require_paths: