compliance_engine 0.5.0 → 0.6.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: 739c21f5b10c86e4ec22f8fdbe5e86175a69f1bfd15af9d064a2fec3c6b11d7b
4
- data.tar.gz: e142e74b1b28997ead592c6c358cc160cd094d307f162adc5f0edf77b3e22f68
3
+ metadata.gz: 9f8203b0a0381216d9a1a91c318f0f162bde1d4835bc7731c954156371b6f9b8
4
+ data.tar.gz: 784264cbb1aba5772bbf97dafdd952c3b8823fa404236c372af87f1d6dad885b
5
5
  SHA512:
6
- metadata.gz: f99f46c33175af5a2bc879ab5254fe4cc91334c11edf334c4809063063fd82279458de34e296d0af4dcc619cd9caa5342f3412f40aded8a73e629137716ea090
7
- data.tar.gz: 0ff070e539cb02ef974de2b2c7de752a79b7e0e0e89f3306bd26780dabe91c836a347f12e8c6e1fe32c4fe0cd2b97c0eefd267ada51a6b22aff64cc5c8151f0f
6
+ metadata.gz: 1026ff097b4e46f31b59b2a584e9d25b94baa986014564fa6a0196d70224306956fd0499d923fa19b0c1035d248c490a3ffaaa1bbcf8f55bd222e7726fb25157
7
+ data.tar.gz: 588997e4f32f95f7d1edef560a16a09399c59d1b7aba4677ca6cf0bf9d97f38568d9d14c9349a5e489288d16dffe0abc2abc0147cecc9beb5127701270d3594a
data/AGENTS.md CHANGED
@@ -91,6 +91,8 @@ A component can have multiple **fragments** (one per source file), which are dee
91
91
  | CE→Control overlap | Any of `check.ces`' CEs has a control that also appears in `profile.controls` |
92
92
  | Direct reference | `profile.checks[check_key]` is truthy |
93
93
 
94
+ An entry on the **profile side** with value `false` is treated as an **explicit exclusion** that overrides positive matches: `profile.checks[X] = false` hard-excludes check `X` from every route, and `profile.ces[X] = false` prevents CE `X` from being used as either a direct CE match or as a CE→Control bridge. On the **check side**, `false` is local-only (e.g. `check.controls[X] = false` simply means the check is not associated with control `X`); `check.ces` is an array, so no `false` handling applies.
95
+
94
96
  `check_mapping` can also be called with CE objects (in addition to profiles). Results are cached by `"#{object.class}:#{object.key}"`.
95
97
 
96
98
  ### Loading Pipeline
data/CHANGELOG.md CHANGED
@@ -1,3 +1,8 @@
1
+ ### 0.6.0 / 2026-05-05
2
+ * Honor explicit `false` in profile `checks` and `ces` mappings as exclusions that override positive matches (#117)
3
+ * Coerce non-String `zipfile.name` to `-` for buffer-opened zips (#121)
4
+ * Replace `Zip::File` handoff with `open_environment_zip_bytes` (#124)
5
+
1
6
  ### 0.5.0 / 2026-04-29
2
7
  * Add JSON Schema for SCE data, exposed via `ComplianceEngine.schema` (#49)
3
8
  * Add `ComplianceEngine::Tolerance` constants for enforcement tolerance levels (#107)
@@ -125,13 +125,29 @@ class ComplianceEngine::Data
125
125
  nil
126
126
  end
127
127
 
128
- # Scan a Puppet environment from a zip file
129
- # @param path [String] The Puppet environment archive file
128
+ # Scan a Puppet environment from a zip file on disk
129
+ # @param path [String] filesystem path to the zip archive
130
+ # @param name [String, nil] stable string identifier used as the modulepath
131
+ # and cache-key prefix; defaults to the full path string passed as +path+.
130
132
  # @return [NilClass]
131
- def open_environment_zip(path)
133
+ def open_environment_zip(path, name: nil)
132
134
  require 'compliance_engine/environment_loader/zip'
133
135
 
134
- environment = ComplianceEngine::EnvironmentLoader::Zip.new(path)
136
+ environment = ComplianceEngine::EnvironmentLoader::Zip.new(path, name: name)
137
+ self.modulepath = environment.modulepath
138
+ open(environment)
139
+ end
140
+
141
+ # Scan a Puppet environment from a raw zip byte string
142
+ #
143
+ # @param bytes [String] raw binary zip data (e.g. from File.binread or an HTTP body)
144
+ # @param name [String, nil] stable string identifier used as the modulepath
145
+ # and cache-key prefix; defaults to "-" when no filename is available.
146
+ # @return [NilClass]
147
+ def open_environment_zip_bytes(bytes, name: nil)
148
+ require 'compliance_engine/environment_loader/zip_bytes'
149
+
150
+ environment = ComplianceEngine::EnvironmentLoader::ZipBytes.new(bytes, name: name)
135
151
  self.modulepath = environment.modulepath
136
152
  open(environment)
137
153
  end
@@ -439,6 +455,9 @@ class ComplianceEngine::Data
439
455
  cache_key = [check.key, "#{profile_or_ce.class}:#{profile_or_ce.key}"].to_s
440
456
  return @mapping[cache_key] if @mapping.key?(cache_key)
441
457
 
458
+ # An explicit `false` on the profile side hard-excludes the check, regardless of any other route.
459
+ return @mapping[cache_key] = false if profile_or_ce.is_a?(ComplianceEngine::Profile) && profile_or_ce.checks&.dig(check.key) == false
460
+
442
461
  # Correlate based on controls
443
462
  controls = check.controls&.select { |_, v| v }&.map { |k, _| k }
444
463
 
@@ -457,9 +476,10 @@ class ComplianceEngine::Data
457
476
  # Correlate based on CEs
458
477
  return @mapping[cache_key] = true if correlate(check.ces, profile_or_ce.ces)
459
478
 
460
- # Correlate based on CEs and controls
461
- return @mapping[cache_key] = true if profile_or_ce.ces&.any? { |k, _| correlate(controls, ces[k]&.controls) }
462
- return @mapping[cache_key] = true if check.ces&.any? { |ce| ces[ce]&.controls&.any? { |k, v| v && profile_or_ce.controls&.dig(k) } }
479
+ # Correlate based on CEs and controls. A CE explicitly set to `false` in the profile
480
+ # cannot be used as a bridge; absence (nil) still allows the bridge.
481
+ return @mapping[cache_key] = true if profile_or_ce.ces&.any? { |k, v| v && correlate(controls, ces[k]&.controls) }
482
+ return @mapping[cache_key] = true if check.ces&.any? { |ce| profile_or_ce.ces&.dig(ce) != false && ces[ce]&.controls&.any? { |k, v| v && profile_or_ce.controls&.dig(k) } }
463
483
 
464
484
  @mapping[cache_key] = false
465
485
  end
@@ -7,23 +7,21 @@ require 'zip/filesystem'
7
7
  # Load compliance engine data from a zip file containing a Puppet environment
8
8
  class ComplianceEngine::EnvironmentLoader::Zip < ComplianceEngine::EnvironmentLoader
9
9
  # Initialize a ComplianceEngine::EnvironmentLoader::Zip object from a zip
10
- # file and an optional root directory.
10
+ # file path and an optional root directory.
11
11
  #
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
12
+ # @param input [String] filesystem path to a zip file
15
13
  # @param root [String] a directory within the zip file to use as the root of the environment
16
14
  # @param load_dotfiles [Boolean] whether to load dotfiles; defaults to true to
17
15
  # preserve the historical zip-loader behaviour of including all files
18
16
  # @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.
17
+ # cache keys; defaults to the full path string passed as +input+.
22
18
  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
19
+ raise ArgumentError, "input must be a String path, got #{input.class}" unless input.is_a?(String)
20
+
21
+ zipfile = ::Zip::File.open(input)
22
+ @modulepath = name || input.to_s
25
23
  super(root, fileclass: zipfile.file, dirclass: zipfile.dir, zipfile_path: @modulepath, load_dotfiles: load_dotfiles)
26
24
  ensure
27
- zipfile.close if zipfile && !input.is_a?(::Zip::File)
25
+ zipfile&.close
28
26
  end
29
27
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../compliance_engine'
4
+ require_relative '../environment_loader'
5
+ # zip/filesystem must be required before any Zip::File instance is opened so
6
+ # that the per-instance dir/file accessors are wired up at open time.
7
+ require 'zip/filesystem'
8
+ require 'zip'
9
+
10
+ # Load compliance engine data from a raw zip byte string containing a Puppet environment
11
+ class ComplianceEngine::EnvironmentLoader::ZipBytes < ComplianceEngine::EnvironmentLoader
12
+ # @param bytes [String] raw binary zip data (e.g. from File.binread or an HTTP body)
13
+ # @param root [String] a directory within the zip file to use as the root of the environment
14
+ # @param load_dotfiles [Boolean] whether to load dotfiles; defaults to true to
15
+ # preserve the historical zip-loader behaviour of including all files
16
+ # @param name [String, nil] identifier used for modulepath and downstream
17
+ # cache keys; defaults to "-" when no filename is available.
18
+ def initialize(bytes, root: '/'.dup, load_dotfiles: true, name: nil)
19
+ raise ArgumentError, "bytes must be a String, got #{bytes.class}" unless bytes.is_a?(String)
20
+
21
+ zipfile = ::Zip::File.open_buffer(bytes)
22
+ @modulepath = name || '-'
23
+ super(root, fileclass: zipfile.file, dirclass: zipfile.dir, zipfile_path: @modulepath, load_dotfiles: load_dotfiles)
24
+ ensure
25
+ zipfile&.close
26
+ end
27
+ end
@@ -51,7 +51,7 @@
51
51
  },
52
52
  "controlsMap": {
53
53
  "type": "object",
54
- "title": "References to top-level controls entries. Enforced when value is true.",
54
+ "title": "References to top-level controls entries. Enforced when value is true; explicitly excluded when value is false.",
55
55
  "patternProperties": {
56
56
  "^\\w(?:[\\w.-]*[\\w-])?(?::\\w(?:[\\w.-]*[\\w-])?)*$": {
57
57
  "type": "boolean",
@@ -132,7 +132,7 @@
132
132
  "ces": {
133
133
  "type": "object",
134
134
  "default": {},
135
- "title": "CEs to include in this profile. Enforced when value is true.",
135
+ "title": "CEs to include in this profile. Enforced when value is true; explicitly excluded when value is false (overrides any positive mapping route).",
136
136
  "patternProperties": {
137
137
  "^\\w(?:[\\w.-]*[\\w-])?(?::\\w(?:[\\w.-]*[\\w-])?)*$": {
138
138
  "type": "boolean",
@@ -151,7 +151,7 @@
151
151
  "checks": {
152
152
  "type": "object",
153
153
  "default": {},
154
- "title": "Checks to include in this profile. Enforced when value is true.",
154
+ "title": "Checks to include in this profile. Enforced when value is true; hard-excluded when value is false (overrides any positive mapping route).",
155
155
  "patternProperties": {
156
156
  "^\\w(?:[\\w.-]*[\\w-])?(?::\\w(?:[\\w.-]*[\\w-])?)*$": {
157
157
  "type": "boolean",
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ComplianceEngine
4
- VERSION = '0.5.0'
4
+ VERSION = '0.6.0'
5
5
  end
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.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Steven Pritchard
@@ -145,6 +145,7 @@ files:
145
145
  - lib/compliance_engine/data_version.rb
146
146
  - lib/compliance_engine/environment_loader.rb
147
147
  - lib/compliance_engine/environment_loader/zip.rb
148
+ - lib/compliance_engine/environment_loader/zip_bytes.rb
148
149
  - lib/compliance_engine/module_loader.rb
149
150
  - lib/compliance_engine/profile.rb
150
151
  - lib/compliance_engine/profiles.rb
@@ -158,7 +159,7 @@ licenses:
158
159
  metadata:
159
160
  homepage_uri: https://simp-project.com/docs/sce/
160
161
  source_code_uri: https://github.com/simp/rubygem-simp-compliance_engine
161
- changelog_uri: https://github.com/simp/rubygem-simp-compliance_engine/releases/tag/0.5.0
162
+ changelog_uri: https://github.com/simp/rubygem-simp-compliance_engine/releases/tag/0.6.0
162
163
  bug_tracker_uri: https://github.com/simp/rubygem-simp-compliance_engine/issues
163
164
  rdoc_options: []
164
165
  require_paths: