compliance_engine 0.2.1 → 0.3.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: bbedee4752b0b7fd84e25194dd8fca02ce5bcf95fe9e36cadae328d82914da63
4
- data.tar.gz: 53536b5e5f5d53ba4d6d7a7679bee7f1b4256e57d7877562f2a336c53588d3ba
3
+ metadata.gz: ca4c111afd1a2840ec2a266bea8e12f195a9cc8c262eef7676280382b63868b5
4
+ data.tar.gz: a59bc32c198332f65882d2a3950cfa2a89c8f48d0da58bf93d77d9e2cc555427
5
5
  SHA512:
6
- metadata.gz: 15a7b6c0d11717ff5b0da2f89fd14207bda9f6a245a0e424a63f0693f8add5aa48e7798911b1058071f024ce45cebc5ce24266302c3dd22c4db402fb7dece58e
7
- data.tar.gz: 9b4354ce4d0bc54fd2284f6c9ff082eb135d1e342e6889673341ff1f66965ad60de504d53638abb05bd4cf7b09d23649e6cf56024da4639068a5f1e3e0b64065
6
+ metadata.gz: 6503e8068deaec3d98bb6783689ae599fa149b3fe2299f1e101ca23f46488db680f4685f36a7670f68c3480977ce9ed539c96d0bb5b6df0f952ea5267d3ed935
7
+ data.tar.gz: 0a0afadab618fa6d86fece7ebb270342e59b788d84b8e321f97c053758a8d0e2b7e68127ccaf4228e2ba65978566fd6ff3bec05b137f829e58ebfc15314b937e
data/AGENTS.md ADDED
@@ -0,0 +1,134 @@
1
+ # AGENTS.md
2
+
3
+ This file provides guidance to AI agents when working with code in this repository.
4
+
5
+ ## Overview
6
+
7
+ This is a Ruby gem (`compliance_engine`) that parses and works with [Sicura/SIMP Compliance Engine (SCE)](https://simp-project.com/docs/sce/) data. It also ships as a Puppet module providing a Hiera backend (`compliance_engine::enforcement`) for enforcing compliance profiles in Puppet environments.
8
+
9
+ ## Commands
10
+
11
+ ### Testing
12
+ ```bash
13
+ # Run all tests and rubocop (default task)
14
+ bundle exec rake
15
+
16
+ # Run just spec tests (with fixture prep/cleanup)
17
+ bundle exec rake spec
18
+
19
+ # Run spec tests standalone (no fixture prep)
20
+ bundle exec rake spec:standalone
21
+
22
+ # Run rubocop linting
23
+ bundle exec rake rubocop
24
+
25
+ # Run a single spec file
26
+ bundle exec rspec spec/classes/compliance_engine/data_spec.rb
27
+
28
+ # Run tests in parallel (used in CI for Ruby < 4.0)
29
+ bundle exec rake parallel_spec
30
+ ```
31
+
32
+ ### Development
33
+ ```bash
34
+ # Install dependencies
35
+ bundle install
36
+
37
+ # Open interactive shell with compliance data loaded
38
+ bundle exec compliance_engine inspect --module /path/to/module
39
+
40
+ # CLI usage examples
41
+ bundle exec compliance_engine profiles --modulepath /path/to/modules
42
+ bundle exec compliance_engine hiera --profile my_profile --modulepath /path/to/modules
43
+ bundle exec compliance_engine lookup some::class::param --profile my_profile --module /path/to/module
44
+ ```
45
+
46
+ ## Architecture
47
+
48
+ ### Data Model
49
+
50
+ Compliance data lives in YAML/JSON files at `<module>/SIMP/compliance_profiles/*.yaml` or `<module>/simp/compliance_profiles/*.yaml`. Files are structured with four top-level keys: `profiles`, `ce` (Compliance Elements), `checks`, and `controls`.
51
+
52
+ The library models this data with a two-layer class hierarchy:
53
+
54
+ **Collections** (`ComplianceEngine::Collection` subclass) hold named groups of components:
55
+ - `ComplianceEngine::Profiles` — keyed by `'profiles'` in source data
56
+ - `ComplianceEngine::Ces` — keyed by `'ce'` in source data
57
+ - `ComplianceEngine::Checks` — keyed by `'checks'` in source data
58
+ - `ComplianceEngine::Controls` — keyed by `'controls'` in source data
59
+
60
+ **Components** (`ComplianceEngine::Component` subclass) represent individual named entries within those collections:
61
+ - `ComplianceEngine::Profile` — a named compliance profile
62
+ - `ComplianceEngine::Ce` — a Compliance Element (CE)
63
+ - `ComplianceEngine::Check` — a single compliance check; only `type: puppet-class-parameter` checks produce Hiera data via `Check#hiera`
64
+ - `ComplianceEngine::Control` — a compliance control
65
+
66
+ A component can have multiple **fragments** (one per source file), which are deep-merged together via `deep_merge`. Confinement logic in `Component` filters fragments based on Puppet facts, module presence/version, and remediation risk level.
67
+
68
+ ### Central Data Object
69
+
70
+ `ComplianceEngine::Data` is the primary entry point. It:
71
+ 1. Loads files via `open(*paths)` which delegates to `ModuleLoader` → `DataLoader::Yaml/Json`
72
+ 2. Uses Ruby's `Observable` pattern — `DataLoader` objects notify `Data` of changes
73
+ 3. Lazily constructs and caches the four collection objects; invalidates all caches when facts, enforcement_tolerance, modulepath, or environment_data change
74
+ 4. Exposes `Data#hiera(profiles)` which walks the check_mapping of requested profiles to produce a flat Hiera-compatible hash
75
+
76
+ ### Business Logic: From Profiles to Hiera
77
+
78
+ **`Data#hiera(profile_names)`** is the primary output method. It:
79
+ 1. Resolves each name to a `Profile` object (logs and skips unknown names).
80
+ 2. Calls `Data#check_mapping(profile)` for each profile to find all associated checks.
81
+ 3. Filters to checks with `type: 'puppet-class-parameter'`.
82
+ 4. Calls `Check#hiera` on each, which returns `{ settings['parameter'] => settings['value'] }`.
83
+ 5. Deep-merges all results into a single flat hash and caches it.
84
+
85
+ **`Data#check_mapping(profile_or_ce)`** is the correlation engine that links profiles (or CEs) to checks. A check is included if **any** of the following hold (evaluated via `Data#mapping?`):
86
+
87
+ | Condition | What it checks |
88
+ |-----------|---------------|
89
+ | Shared **control** | `check.controls` and `profile.controls` share a key set to `true` |
90
+ | Shared **CE** | `check.ces` and `profile.ces` share a key set to `true` |
91
+ | CE→Control overlap | Any of `check.ces`' CEs has a control that also appears in `profile.controls` |
92
+ | Direct reference | `profile.checks[check_key]` is truthy |
93
+
94
+ `check_mapping` can also be called with CE objects (in addition to profiles). Results are cached by `"#{object.class}:#{object.key}"`.
95
+
96
+ ### Loading Pipeline
97
+
98
+ ```
99
+ paths → EnvironmentLoader → ModuleLoader (one per module dir)
100
+ → DataLoader::Yaml / DataLoader::Json
101
+ ↓ (Observable notify)
102
+ ComplianceEngine::Data#update
103
+ ```
104
+
105
+ - `EnvironmentLoader` scans a Puppet modulepath for module directories
106
+ - `EnvironmentLoader::Zip` handles zip-archived environments
107
+ - `ModuleLoader` reads a module's `metadata.json` and discovers compliance data files
108
+ - `DataLoader` (and its subclasses) read and parse individual files; they use the Observable pattern to push updates to `Data`
109
+
110
+ ### Puppet Hiera Backend
111
+
112
+ `lib/puppet/functions/compliance_engine/enforcement.rb` implements the Hiera `lookup_key` function. It:
113
+ - Resolves profiles from `compliance_engine::enforcement` and optionally `compliance_markup::enforcement` Hiera keys
114
+ - Creates and caches a `ComplianceEngine::Data` object on the Puppet lookup context
115
+ - Calls `data.hiera(profiles)` and bulk-caches results for subsequent lookups
116
+ - Supports `compliance_markup` backwards compatibility via `compliance_markup_compatibility` option
117
+
118
+ ### Confinement and Enforcement Tolerance
119
+
120
+ `Component#fragments` filters source fragments based on:
121
+ - **Fact confinement** (`confine` key): dot-notation Puppet facts (e.g. `os.release.major`). Values may be a string (exact match), a string prefixed with `!` (negation), or an array (any match). Implemented in `Component#fact_match?`. Fact confinement is skipped when `facts` is `nil`.
122
+ - **Module confinement** (`confine.module_name` + `confine.module_version`): checks against `environment_data` (a `{module_name => version}` hash) using semantic versioning. Module confinement only runs when `environment_data` is set (e.g. by `ComplianceEngine::Data#open`).
123
+ - **Remediation risk** (`remediation.risk`): when `enforcement_tolerance` is a positive `Integer`, drops fragments where risk level ≥ `enforcement_tolerance` and drops disabled remediations. Only applies to `Check` components.
124
+
125
+ In practice, only fact confinement is bypassed when `facts` is `nil`; module confinement still applies whenever `environment_data` is available. All confinement and risk/disabled-remediation filtering are effectively bypassed only when both `facts` and `environment_data` are unset and `enforcement_tolerance` is not a positive `Integer` (every fragment is then included). This is useful for offline analysis where system context and enforcement settings are unavailable.
126
+
127
+ ### Code Style
128
+
129
+ Rubocop is configured via `.rubocop.yml` inheriting from `voxpupuli-test`. Key style choices:
130
+ - `compact` class/module nesting style (e.g. `class ComplianceEngine::Data` not nested modules)
131
+ - Trailing commas on multiline args/arrays
132
+ - Leading dot position for method chaining
133
+ - `braces_for_chaining` block delimiters
134
+ - Max line length: 200
data/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ ### 0.3.0 / 2026-03-19
2
+ * Hash-like Collection methods return Collection objects (#37)
3
+
4
+ ### 0.2.2 / 2026-03-02
5
+ * Ensure that cloned/duped objects get independent collection instances
6
+
1
7
  ### 0.2.1 / 2026-02-12
2
8
  * Turn off debugging in compliance_engine::enforcement function
3
9
 
data/README.md CHANGED
@@ -42,6 +42,54 @@ Options:
42
42
 
43
43
  See the [`ComplianceEngine::Data`](https://rubydoc.info/gems/compliance_engine/ComplianceEngine/Data) class for details.
44
44
 
45
+ ## Concepts
46
+
47
+ ### Data Model
48
+
49
+ Compliance data is expressed across four entity types that live in YAML/JSON files inside Puppet modules (`<module>/SIMP/compliance_profiles/*.yaml`):
50
+
51
+ | Entity | Key | Purpose |
52
+ |--------|-----|---------|
53
+ | **Profile** | `profiles` | A named compliance standard (e.g. `nist_800_53_rev4`). References CEs, checks, and/or controls that together constitute that standard. |
54
+ | **CE** (Compliance Element) | `ce` | A single, named compliance capability (e.g. "enable audit logging"). Bridges profiles to checks via a shared vocabulary. |
55
+ | **Check** | `checks` | A verifiable assertion about a system setting. Checks of `type: puppet-class-parameter` carry a `parameter` and `value` that become Hiera data. |
56
+ | **Control** | `controls` | A cross-reference label from an external framework (e.g. `nist_800_53:rev4:AU-2`). Profiles and checks both annotate themselves with controls to express alignment. |
57
+
58
+ ### From Profiles to Hiera Data
59
+
60
+ The central operation of the library is `Data#hiera(profiles)`, which converts a list of profile names into a flat hash of Puppet class parameters and their enforced values:
61
+
62
+ ```
63
+ profile names
64
+ ↓ check_mapping: find all checks that belong to each profile
65
+ checks (type: puppet-class-parameter only)
66
+ ↓ Check#hiera: extract { 'class::param' => value }
67
+ deep-merged hash → { 'widget_spinner::audit_logging' => true, ... }
68
+ ```
69
+
70
+ **How check_mapping works** — a check is considered part of a profile if any of the following are true:
71
+
72
+ 1. The check and profile share a **control** label (`nist_800_53:rev4:AU-2`).
73
+ 2. The check and profile share a **CE** reference.
74
+ 3. The check's CE and the profile share a **control** label.
75
+ 4. The profile explicitly lists the check by key under its `checks:` map.
76
+
77
+ This layered matching lets compliance authors express mappings at different levels of abstraction and have the engine resolve them automatically.
78
+
79
+ ### Confinement
80
+
81
+ A component (profile, CE, check, or control) may be defined across multiple source files. Each file contributes a **fragment**. Before fragments are merged, they are filtered by:
82
+
83
+ - **Facts** (`confine:` key): dot-notation Puppet facts, optionally negated with a `!` prefix. A fragment is dropped if its confinement does not match the current system's facts.
84
+ - **Module presence/version** (`confine.module_name` / `confine.module_version`): fragment is dropped if the required module is absent or the wrong version.
85
+ - **Remediation risk/status** (`remediation.risk` / `remediation.disabled`): when `enforcement_tolerance` is set to a positive Integer, a fragment is dropped if remediation is explicitly `disabled` or if its risk level is ≥ `enforcement_tolerance`.
86
+
87
+ If `facts` is `nil`, all fact/module confinement is skipped; fragments are still subject to remediation-based filtering when `enforcement_tolerance` is set.
88
+
89
+ ### Enforcement Tolerance
90
+
91
+ `enforcement_tolerance` is an optional integer threshold that controls how cautiously the engine applies remediations. When it is set to a positive Integer, fragments whose `remediation.risk.level` meets or exceeds the threshold, or whose remediation is explicitly `disabled`, are silently excluded from the merged result, allowing operators to tune aggressiveness (e.g. apply only low-risk remediations in production, all remediations in a test environment). When `enforcement_tolerance` is `nil` or not a positive Integer, no remediation-based filtering occurs and `remediation.risk` / `remediation.disabled` do not affect fragment inclusion.
92
+
45
93
  ## Using as a Puppet Module
46
94
 
47
95
  The Compliance Engine can be used as a Puppet module to provide a Hiera backend for compliance data. This allows you to enforce compliance profiles through Hiera lookups within your Puppet manifests.
@@ -32,6 +32,27 @@ class ComplianceEngine::Collection
32
32
  nil
33
33
  end
34
34
 
35
+ # Ensure that cloned/duped objects get independent component instances.
36
+ #
37
+ # Ruby's default clone/dup is a shallow copy, so @collection would be
38
+ # shared between the source and the copy, and every Component inside it
39
+ # would be the same object. Calling invalidate_cache on one copy would
40
+ # then propagate its facts into those shared components, silently
41
+ # affecting the other copy's view of the data.
42
+ #
43
+ # Duping each Component (which triggers Component#initialize_copy) gives
44
+ # every copy of the Collection its own independent components. Any
45
+ # derived caches on the Collection itself (e.g. @by_oval_id in Ces) are
46
+ # cleared so each copy rebuilds them from its own component set.
47
+ #
48
+ # @return [NilClass]
49
+ def initialize_copy(_source)
50
+ super
51
+ @collection = @collection.transform_values(&:dup)
52
+ (instance_variables - (context_variables + [:@collection])).each { |var| instance_variable_set(var, nil) }
53
+ nil
54
+ end
55
+
35
56
  # Converts the object to a hash representation
36
57
  #
37
58
  # @return [Hash] the hash representation of the object
@@ -46,6 +67,13 @@ class ComplianceEngine::Collection
46
67
  to_h.keys
47
68
  end
48
69
 
70
+ # Returns the values of the collection
71
+ #
72
+ # @return [Array] the values of the collection
73
+ def values
74
+ to_h.values
75
+ end
76
+
49
77
  # Return a single value from the collection
50
78
  #
51
79
  # @param key [String] the key of the value to return
@@ -57,22 +85,34 @@ class ComplianceEngine::Collection
57
85
  # Iterates over the collection
58
86
  #
59
87
  # @param block [Proc] the block to execute
88
+ # @return [self, Enumerator]
60
89
  def each(&block)
90
+ return to_enum(:each) unless block
91
+
61
92
  to_h.each(&block)
93
+ self
62
94
  end
63
95
 
64
96
  # Iterates over values in the collection
65
97
  #
66
98
  # @param block [Proc] the block to execute
99
+ # @return [self, Enumerator]
67
100
  def each_value(&block)
101
+ return to_enum(:each_value) unless block
102
+
68
103
  to_h.each_value(&block)
104
+ self
69
105
  end
70
106
 
71
107
  # Iterates over keys in the collection
72
108
  #
73
109
  # @param block [Proc] the block to execute
110
+ # @return [self, Enumerator]
74
111
  def each_key(&block)
112
+ return to_enum(:each_key) unless block
113
+
75
114
  to_h.each_key(&block)
115
+ self
76
116
  end
77
117
 
78
118
  # Return true if any of the values in the collection match the block
@@ -94,17 +134,35 @@ class ComplianceEngine::Collection
94
134
  # Select values in the collection
95
135
  #
96
136
  # @param block [Proc] the block to execute
97
- # @return [Hash] the filtered hash
137
+ # @return [ComplianceEngine::Collection, Enumerator] the filtered collection or an Enumerator when no block is given
98
138
  def select(&block)
99
- to_h.select(&block)
139
+ return to_enum(:select) unless block_given?
140
+
141
+ result = dup
142
+ result.collection = result.to_h.select(&block)
143
+ result
100
144
  end
101
145
 
146
+ alias filter select
147
+
102
148
  # Filter out values in the collection
103
149
  #
104
150
  # @param block [Proc] the block to execute
105
- # @return [Hash] the filtered hash
151
+ # @return [ComplianceEngine::Collection, Enumerator] the filtered collection or an Enumerator when no block is given
106
152
  def reject(&block)
107
- to_h.reject(&block)
153
+ return to_enum(:reject) unless block_given?
154
+
155
+ result = dup
156
+ result.collection = result.to_h.reject(&block)
157
+ result
158
+ end
159
+
160
+ # Transform values in the collection
161
+ #
162
+ # @param block [Proc] the block to execute
163
+ # @return [Hash, Enumerator] a hash with transformed values, or an Enumerator when no block is given
164
+ def transform_values(&block)
165
+ to_h.transform_values(&block)
108
166
  end
109
167
 
110
168
  private
@@ -26,6 +26,29 @@ class ComplianceEngine::Component
26
26
  nil
27
27
  end
28
28
 
29
+ # Ensure that cloned/duped objects get independent fragment stores.
30
+ #
31
+ # Ruby's default clone/dup is a shallow copy, so @component (and the
32
+ # fragments hash nested within it) would be shared between the source
33
+ # and the copy. Calling add on either would write into the same
34
+ # fragments hash, making new fragments visible on both objects.
35
+ # Pre-computed @element and @fragments caches would also be shared,
36
+ # returning stale fact-filtered results on the copy even after facts
37
+ # are changed.
38
+ #
39
+ # Duping @component and its inner fragments hash ensures each copy has
40
+ # an independent fragment store. Cache variables are cleared so each
41
+ # copy rebuilds them lazily from its own fragments on first access.
42
+ #
43
+ # @return [NilClass]
44
+ def initialize_copy(_source)
45
+ super
46
+ @component = @component.dup
47
+ @component[:fragments] = @component[:fragments].transform_values { |fragment| Marshal.load(Marshal.dump(fragment)) }
48
+ cache_variables.each { |var| instance_variable_set(var, nil) }
49
+ nil
50
+ end
51
+
29
52
  # Adds a value to the fragments array of the component.
30
53
  #
31
54
  # @param value [Object] The value to be added to the fragments array.
@@ -250,7 +273,7 @@ class ComplianceEngine::Component
250
273
  @element = {}
251
274
 
252
275
  fragments.each_value do |fragment|
253
- @element = DeepMerge.deep_merge!(fragment, @element)
276
+ @element = DeepMerge.deep_merge!(Marshal.load(Marshal.dump(fragment)), @element)
254
277
  end
255
278
 
256
279
  @element
@@ -88,6 +88,42 @@ class ComplianceEngine::Data
88
88
  (instance_variables - (data_variables + context_variables)).each { |var| instance_variable_set(var, nil) }
89
89
  end
90
90
 
91
+ # Ensure that cloned/duped objects get independent collection instances.
92
+ #
93
+ # Ruby's default clone/dup is a shallow copy, so the collection instance
94
+ # variables (@ces, @profiles, @checks, @controls) would otherwise point to
95
+ # the same objects as the source. When facts= is later called on either the
96
+ # source or the clone, invalidate_cache propagates facts into the shared
97
+ # collection, causing the other object to silently adopt the wrong facts.
98
+ #
99
+ # Nilling the collection variables here forces each clone to lazily rebuild
100
+ # its own collections the first time they are accessed, using its own context
101
+ # (facts, enforcement_tolerance, etc.). Cache variables that depend on those
102
+ # collections are cleared for the same reason.
103
+ #
104
+ # @return [NilClass]
105
+ def initialize_copy(_source)
106
+ super
107
+ # Give each clone its own outer @data hash and its own per-file inner
108
+ # hashes so that new files opened on one clone (via open/update) are not
109
+ # visible to other clones or the source, and so that a loader refresh on
110
+ # the source (which mutates the inner hash in-place via Data#update) does
111
+ # not silently affect a clone that has not yet built its lazy collections.
112
+ # The inner per-file content values (read-only parsed data) stay shared.
113
+ #
114
+ # :loader is additionally cleared (set to nil) so the copy does not hold
115
+ # a reference to the source's DataLoader object. If it did, the copy
116
+ # calling update(key_string) for an already-known file would invoke
117
+ # loader.refresh, which notifies the source (the registered Observable
118
+ # observer) and overwrites source.data[key][:content] while the copy's
119
+ # inner hash stays stale. With :loader nil the copy creates its own
120
+ # independent loader (and registers itself as observer) on next access.
121
+ @data = @data.transform_values { |entry| entry.merge(loader: nil) }
122
+ collection_variables.each { |var| instance_variable_set(var, nil) }
123
+ cache_variables.each { |var| instance_variable_set(var, nil) }
124
+ nil
125
+ end
126
+
91
127
  # Scan a Puppet environment from a zip file
92
128
  # @param path [String] The Puppet environment archive file
93
129
  # @return [NilClass]
@@ -191,8 +227,13 @@ class ComplianceEngine::Data
191
227
  else
192
228
  data[filename.key] ||= {}
193
229
 
194
- # Assume filename is a loader object
195
- unless data[filename.key]&.key?(:loader)
230
+ # Register as an observer only when no loader is currently attached.
231
+ # Checking the :loader value (rather than key presence) is important
232
+ # after clone/dup: initialize_copy sets :loader to nil so the copy does
233
+ # not share the source's loader, but the key still exists. Checking
234
+ # key presence would see the nil as "already registered" and skip
235
+ # add_observer, leaving the copy deaf to future loader refreshes.
236
+ unless data[filename.key][:loader]
196
237
  data[filename.key][:loader] = filename
197
238
  data[filename.key][:loader].add_observer(self, :update)
198
239
  end
@@ -20,13 +20,17 @@ class ComplianceEngine::DataLoader
20
20
 
21
21
  # Set the data for the data loader
22
22
  #
23
- # @param value [Hash] The new value for the data loader
23
+ # The hash and all nested hashes, arrays, and strings within it are
24
+ # deep-frozen so that parsed compliance data is treated as read-only
25
+ # once loaded. Callers must not retain a mutable reference to the
26
+ # hash after calling this method.
24
27
  #
28
+ # @param value [Hash] The new value for the data loader
25
29
  # @raise [ComplianceEngine::Error] If the value is not a Hash
26
30
  def data=(value)
27
31
  raise ComplianceEngine::Error, 'Data must be a hash' unless value.is_a?(Hash)
28
32
 
29
- @data = value
33
+ @data = deep_freeze(value)
30
34
  changed
31
35
  notify_observers(self)
32
36
  end
@@ -43,4 +47,24 @@ class ComplianceEngine::DataLoader
43
47
  require 'securerandom'
44
48
  @key = "#{data.class}:#{SecureRandom.uuid}"
45
49
  end
50
+
51
+ private
52
+
53
+ # Recursively freezes a Hash or Array and all nested objects.
54
+ #
55
+ # Parsed compliance data is read-only once loaded; deep-freezing it makes
56
+ # that invariant explicit and surfaces any accidental in-place mutation
57
+ # immediately as a FrozenError rather than silent data corruption.
58
+ #
59
+ # @param obj [Object] the object to freeze
60
+ # @return [Object] the frozen object (modified in-place)
61
+ def deep_freeze(obj)
62
+ case obj
63
+ when Hash
64
+ obj.each_value { |v| deep_freeze(v) }
65
+ when Array
66
+ obj.each { |v| deep_freeze(v) }
67
+ end
68
+ obj.freeze
69
+ end
46
70
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ComplianceEngine
4
- VERSION = '0.2.1'
4
+ VERSION = '0.3.0'
5
5
 
6
6
  # Handle supported compliance data versions
7
7
  class Version
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.2.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Steven Pritchard
@@ -120,6 +120,7 @@ executables:
120
120
  extensions: []
121
121
  extra_rdoc_files: []
122
122
  files:
123
+ - AGENTS.md
123
124
  - CHANGELOG.md
124
125
  - LICENSE
125
126
  - README.md
@@ -153,7 +154,7 @@ licenses:
153
154
  metadata:
154
155
  homepage_uri: https://simp-project.com/docs/sce/
155
156
  source_code_uri: https://github.com/simp/rubygem-simp-compliance_engine
156
- changelog_uri: https://github.com/simp/rubygem-simp-compliance_engine/releases/tag/0.2.1
157
+ changelog_uri: https://github.com/simp/rubygem-simp-compliance_engine/releases/tag/0.3.0
157
158
  bug_tracker_uri: https://github.com/simp/rubygem-simp-compliance_engine/issues
158
159
  rdoc_options: []
159
160
  require_paths:
@@ -169,7 +170,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
169
170
  - !ruby/object:Gem::Version
170
171
  version: '0'
171
172
  requirements: []
172
- rubygems_version: 4.0.3
173
+ rubygems_version: 4.0.6
173
174
  specification_version: 4
174
175
  summary: Parser for Sicura Compliance Engine data
175
176
  test_files: []