compliance_engine 0.2.2 → 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: fce58aedae595351e68af63ff5af15c884bb0b8449c3ec4e2ebb71ebbad873d9
4
- data.tar.gz: cf5c54d0cde492c74df83bfd7701779c8db1a79a289ca34825f868faacffab03
3
+ metadata.gz: ca4c111afd1a2840ec2a266bea8e12f195a9cc8c262eef7676280382b63868b5
4
+ data.tar.gz: a59bc32c198332f65882d2a3950cfa2a89c8f48d0da58bf93d77d9e2cc555427
5
5
  SHA512:
6
- metadata.gz: 35511b511226cdd62a750d3d39e8f40cc9edece3fc72cc310356b05da62679b6ca241ee4d60f88eef731e4ce252627436288414b95f32f3125ce2918f9d9a61b
7
- data.tar.gz: fef7c63498f348f9d92d2c61e35d00e59822a59cfad779244d948b10c744a026eb304a23ff40da90d26142a710a775f0370697755e790563f6ba8736cf218ec3
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,6 @@
1
+ ### 0.3.0 / 2026-03-19
2
+ * Hash-like Collection methods return Collection objects (#37)
3
+
1
4
  ### 0.2.2 / 2026-03-02
2
5
  * Ensure that cloned/duped objects get independent collection instances
3
6
 
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.
@@ -67,6 +67,13 @@ class ComplianceEngine::Collection
67
67
  to_h.keys
68
68
  end
69
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
+
70
77
  # Return a single value from the collection
71
78
  #
72
79
  # @param key [String] the key of the value to return
@@ -78,22 +85,34 @@ class ComplianceEngine::Collection
78
85
  # Iterates over the collection
79
86
  #
80
87
  # @param block [Proc] the block to execute
88
+ # @return [self, Enumerator]
81
89
  def each(&block)
90
+ return to_enum(:each) unless block
91
+
82
92
  to_h.each(&block)
93
+ self
83
94
  end
84
95
 
85
96
  # Iterates over values in the collection
86
97
  #
87
98
  # @param block [Proc] the block to execute
99
+ # @return [self, Enumerator]
88
100
  def each_value(&block)
101
+ return to_enum(:each_value) unless block
102
+
89
103
  to_h.each_value(&block)
104
+ self
90
105
  end
91
106
 
92
107
  # Iterates over keys in the collection
93
108
  #
94
109
  # @param block [Proc] the block to execute
110
+ # @return [self, Enumerator]
95
111
  def each_key(&block)
112
+ return to_enum(:each_key) unless block
113
+
96
114
  to_h.each_key(&block)
115
+ self
97
116
  end
98
117
 
99
118
  # Return true if any of the values in the collection match the block
@@ -115,17 +134,35 @@ class ComplianceEngine::Collection
115
134
  # Select values in the collection
116
135
  #
117
136
  # @param block [Proc] the block to execute
118
- # @return [Hash] the filtered hash
137
+ # @return [ComplianceEngine::Collection, Enumerator] the filtered collection or an Enumerator when no block is given
119
138
  def select(&block)
120
- 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
121
144
  end
122
145
 
146
+ alias filter select
147
+
123
148
  # Filter out values in the collection
124
149
  #
125
150
  # @param block [Proc] the block to execute
126
- # @return [Hash] the filtered hash
151
+ # @return [ComplianceEngine::Collection, Enumerator] the filtered collection or an Enumerator when no block is given
127
152
  def reject(&block)
128
- 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)
129
166
  end
130
167
 
131
168
  private
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ComplianceEngine
4
- VERSION = '0.2.2'
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.2
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.2
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: []