compliance 0.0.2 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a8f7ad92baa85021ad2f6d1fc1f38fe8b0661cbcfdc1a186d57a786fe66314a0
4
- data.tar.gz: e72c020cae731fe2d6352e0eb994417ff5cfafe98f016b0ac9b87ab3fd0fc235
3
+ metadata.gz: 3334cb98507d29e5249dc7f542ee2acb68c5dd0fb5a995b0303f2636da8bc6b2
4
+ data.tar.gz: 2154be63063316e33ca4046d317c7a2a68d20cad649640fe2c10f07358dc7301
5
5
  SHA512:
6
- metadata.gz: d1e1049191d15e7ab231a33350099e5367ab4fd2a97028a86f39bd16586147c11272083020b9b754288736b3927a9a71555cd91c7e303afadaf1b01bb2a7c4f7
7
- data.tar.gz: 03a598d1cf4378bf12d8c5469dd3421165d3c220a1261ee63219281325990df8b4ed270ab4005fbd98086e32d2a1c8a2135a8fe03a76623f9a517acef8260bd5
6
+ metadata.gz: 70cd172b262c2caa19616dda3fb9ab176baff4a187818bf949f6edc30a1fcdf241409fe4c6435a4d28f326b1fc87f1fd1d85d44776dd3b70e9908b584cf427ab
7
+ data.tar.gz: 6de40f9d88eff321ed532a124a18a54e7af2f07f61506641fb956d846332da183ed179a273e7aab52f3771063363978e430b1999f8111121a6260bfeb90e2666
checksums.yaml.gz.sig CHANGED
Binary file
data/bake/compliance.rb CHANGED
@@ -9,27 +9,29 @@ def initialize(...)
9
9
  require 'compliance'
10
10
  end
11
11
 
12
- def document
13
- document = Compliance::Document.new
12
+ # Load the default compliance policy.
13
+ def policy
14
+ loader = Compliance::Loader.default([context.root])
14
15
 
15
- ::Gem.loaded_specs.each do |name, spec|
16
- Console.logger.debug(self) {"Checking gem #{name}: #{spec.full_gem_path}..."}
17
-
18
- if path = spec.full_gem_path and File.directory?(path)
19
- Compliance::Loader.load(path, document)
20
- end
21
- end
22
-
23
- return document
16
+ return Compliance::Policy.default(loader)
24
17
  end
25
18
 
26
- def check(input:)
27
- policy = Compliance::Policy.new
28
- document = input || self.document
19
+ # List available compliance documents.
20
+ def list
21
+ loader = Compliance::Loader.default([context.root])
29
22
 
23
+ loader.cache.map do |name, path|
24
+ {name: name, path: path}
25
+ end
26
+ end
27
+
28
+ # Check compliance with the policy.
29
+ def check
30
+ policy = self.policy
31
+
30
32
  failed_requirements = {}
31
33
 
32
- document.check(policy) do |requirement, satisfied, unsatisfied|
34
+ results = policy.check do |requirement, satisfied, unsatisfied|
33
35
  Console.debug(self) {"Requirement #{requirement.id} is #{satisfied.any? ? "satisfied." : "not satisfied!"}"}
34
36
 
35
37
  if satisfied.empty?
@@ -37,5 +39,53 @@ def check(input:)
37
39
  end
38
40
  end
39
41
 
40
- raise Compliance::Error.new(failed_requirements) unless failed_requirements.empty?
42
+ if failed_requirements.any?
43
+ raise Compliance::Error.new(failed_requirements)
44
+ else
45
+ Console.debug(self) {"All requirements are satisfied."}
46
+ return results
47
+ end
48
+ end
49
+
50
+ # Attest to a requirement.
51
+ # @parameter id [String] The unique identifier for the attestation, matching the requirement.
52
+ # @parameter description [String] A description of how the requirement is satisfied.
53
+ # @parameter by [String] The entity attesting to the requirement.
54
+ def attest(id, description: nil, by: nil)
55
+ compliance_root = Compliance::Document.path(context.root)
56
+
57
+ if File.exist?(compliance_root)
58
+ document = Compliance::Document.load(compliance_root)
59
+ else
60
+ document = Compliance::Document.new
61
+ end
62
+
63
+ attestation = self.attestation_for(id, document)
64
+
65
+ if description
66
+ attestation.metadata[:description] = description
67
+ end
68
+
69
+ if by
70
+ attestation.metadata[:by] = by
71
+ end
72
+
73
+ File.write(compliance_root, JSON.pretty_generate(document))
74
+
75
+ return attestation
76
+ end
77
+
78
+ private
79
+
80
+ def attestation_for(id, document)
81
+ document.attestations.each do |attestation|
82
+ if attestation.id == id
83
+ return attestation
84
+ end
85
+ end
86
+
87
+ attestation = Compliance::Attestation.new(id: id)
88
+ document.attestations << attestation
89
+
90
+ return attestation
41
91
  end
@@ -6,6 +6,9 @@
6
6
  module Compliance
7
7
  # Represents an attestation of compliance with a requirement.
8
8
  class Attestation
9
+ class Error < StandardError
10
+ end
11
+
9
12
  def initialize(metadata)
10
13
  @metadata = metadata
11
14
  end
@@ -25,5 +28,9 @@ module Compliance
25
28
 
26
29
  # The metadata associated with this attestation.
27
30
  attr :metadata
31
+
32
+ def [] key
33
+ @metadata[key]
34
+ end
28
35
  end
29
36
  end
@@ -3,65 +3,63 @@
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2024, by Samuel Williams.
5
5
 
6
+ require_relative 'import'
6
7
  require_relative 'requirement'
7
8
  require_relative 'attestation'
8
9
 
9
10
  module Compliance
10
11
  # Represents a document containing requirements and attestations.
11
12
  class Document
12
- def initialize
13
- @requirements = []
14
- @attestations = []
13
+ def self.path(root)
14
+ File.expand_path("compliance.json", root)
15
+ end
16
+
17
+ def self.load(path)
18
+ data = JSON.load_file(path, symbolize_names: true)
15
19
 
16
- @attestations_by_id = {}
20
+ self.new.tap do |document|
21
+ data[:imports]&.each do |import|
22
+ document.imports << Import.new(import)
23
+ end
24
+
25
+ data[:requirements]&.each do |metadata|
26
+ document.requirements << Requirement.new(metadata)
27
+ end
28
+
29
+ data[:attestations]&.each do |metadata|
30
+ document.attestations << Attestation.new(metadata)
31
+ end
32
+ end
33
+ end
34
+
35
+ def initialize(imports: [], requirements: [], attestations: [])
36
+ @imports = imports
37
+ @requirements = requirements
38
+ @attestations = attestations
17
39
  end
18
40
 
19
41
  def as_json(...)
20
- {
21
- requirements: @requirements,
22
- attestations: @attestations
23
- }
42
+ Hash.new.tap do |hash|
43
+ if @imports.any?
44
+ hash[:imports] = @imports.map(&:as_json)
45
+ end
46
+
47
+ if @requirements.any?
48
+ hash[:requirements] = @requirements.map(&:as_json)
49
+ end
50
+
51
+ if @attestations.any?
52
+ hash[:attestations] = @attestations.map(&:as_json)
53
+ end
54
+ end
24
55
  end
25
56
 
26
57
  def to_json(...)
27
58
  as_json.to_json(...)
28
59
  end
29
60
 
61
+ attr :imports
30
62
  attr :requirements
31
63
  attr :attestations
32
-
33
- # Add a requirement to the document.
34
- def add_requirement(requirement)
35
- @requirements << requirement
36
- end
37
-
38
- # Add an attestation to the document.
39
- # @parameter attestation [Attestation] The attestation to add to the document.
40
- def add_attestation(attestation)
41
- @attestations << attestation
42
- (@attestations_by_id[attestation.id] ||= Array.new) << attestation
43
- end
44
-
45
- # Check the document against a given policy.
46
- def check(policy)
47
- return to_enum(:check, policy) unless block_given?
48
-
49
- @requirements.each do |requirement|
50
- attestations = @attestations_by_id[requirement.id]
51
-
52
- satisfied = []
53
- unsatisfied = []
54
-
55
- attestations&.each do |attestation|
56
- if policy.satisfies?(requirement, attestation)
57
- satisfied << attestation
58
- else
59
- unsatisfied << attestation
60
- end
61
- end
62
-
63
- yield requirement, satisfied, unsatisfied
64
- end
65
- end
66
64
  end
67
65
  end
@@ -7,7 +7,24 @@ module Compliance
7
7
  # Represents an error that occurs when requirements are not satisfied.
8
8
  class Error < StandardError
9
9
  def initialize(unsatisfied)
10
- super "Unsatisfied requirements: #{unsatisfied.keys.join(', ')}"
10
+ super "#{unsatisfied.size} unsatisfied requirement(s): #{unsatisfied.keys.join(', ')}"
11
+
12
+ @unsatisfied = unsatisfied
13
+ end
14
+
15
+ attr :unsatisfied
16
+
17
+ def detailed_message(...)
18
+ buffer = String.new
19
+ buffer << super << "\n"
20
+
21
+ @unsatisfied.map do |id, requirement|
22
+ if description = requirement[:description]
23
+ buffer << "\t- #{id}: #{description}" << "\n"
24
+ end
25
+ end
26
+
27
+ buffer
11
28
  end
12
29
  end
13
30
  end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2024, by Samuel Williams.
5
+
6
+ module Compliance
7
+ # Represents an attestation of compliance with a requirement.
8
+ class Import
9
+ class Error < StandardError
10
+ end
11
+
12
+ def initialize(name)
13
+ @name = name
14
+ end
15
+
16
+ def as_json(...)
17
+ @name
18
+ end
19
+
20
+ def to_json(...)
21
+ as_json.to_json(...)
22
+ end
23
+
24
+ attr :name
25
+
26
+ def resolve(policy, loader)
27
+ loader.import(@name, policy)
28
+ end
29
+ end
30
+ end
@@ -10,34 +10,88 @@ require 'json'
10
10
 
11
11
  module Compliance
12
12
  # Load compliance data from JSON files.
13
- module Loader
14
- def self.add(compliance, document)
15
- compliance[:requirements]&.each do |metadata|
16
- document.add_requirement(Requirement.new(metadata))
13
+ class Loader
14
+ class Error < StandardError
15
+ end
16
+
17
+ # Setup the default loader.
18
+ def self.default(roots = [Dir.pwd])
19
+ ::Gem.loaded_specs.each do |name, spec|
20
+ if path = spec.full_gem_path and File.directory?(path)
21
+ compliance_path = File.expand_path("compliance.json", path)
22
+ compliance_directory = File.expand_path("compliance", path)
23
+
24
+ if File.file?(compliance_path) or File.directory?(compliance_directory)
25
+ roots << path
26
+ end
27
+ end
17
28
  end
18
29
 
19
- compliance[:attestations]&.each do |metadata|
20
- document.add_attestation(Attestation.new(metadata))
30
+ roots.uniq!
31
+
32
+ self.new(roots)
33
+ end
34
+
35
+ def initialize(roots = [])
36
+ @roots = roots
37
+ @cache = nil
38
+ end
39
+
40
+ # List of root directories to search for compliance documents.
41
+ attr :roots
42
+
43
+ # Cache of name to path mappings.
44
+ def cache
45
+ @cache ||= build_cache
46
+ end
47
+
48
+ # Map a name to a path.
49
+ def resolve(name)
50
+ cache[name]
51
+ end
52
+
53
+ # Import a named document into the policy.
54
+ def import(name, policy)
55
+ if path = cache[name]
56
+ begin
57
+ document = Document.load(path)
58
+ rescue => error
59
+ raise Error, "Error loading compliance document: #{path}!"
60
+ end
61
+
62
+ begin
63
+ policy.add(document, self)
64
+ rescue => error
65
+ raise Error, "Error adding compliance document to policy: #{path}!"
66
+ end
67
+ else
68
+ raise Error, "Could not find import: #{name}"
21
69
  end
22
70
  end
23
71
 
24
- # Load compliance data from a directory.
25
- # @parameter root [String] The root directory to load compliance data from.
26
- # @parameter document [Document] The document to load compliance data into.
27
- def self.load(root, document)
28
- compliance_json_path = File.expand_path("compliance.json", root)
29
-
30
- if File.file?(compliance_json_path)
31
- data = JSON.load_file(compliance_json_path, symbolize_names: true)
32
- self.add(data, document)
72
+ # Load all top level documents.
73
+ def documents
74
+ @roots.filter_map do |path|
75
+ compliance_path = File.expand_path("compliance.json", path)
76
+ if File.file?(compliance_path)
77
+ begin
78
+ Document.load(compliance_path)
79
+ rescue => error
80
+ raise Error, "Error loading compliance document: #{compliance_path}!"
81
+ end
82
+ end
33
83
  end
34
-
35
- compliance_directory = File.expand_path("compliance", root)
36
-
37
- if File.directory?(compliance_directory)
38
- Dir.glob(File.join(compliance_directory, "*.json")) do |path|
39
- data = JSON.load_file(path, symbolize_names: true)
40
- self.add(data, document)
84
+ end
85
+
86
+ protected
87
+
88
+ def build_cache
89
+ Hash.new.tap do |cache|
90
+ @roots.each do |root|
91
+ Dir.glob(File.expand_path("compliance/*.json", root)) do |path|
92
+ name = File.basename(path, ".json")
93
+ cache[name] = path
94
+ end
41
95
  end
42
96
  end
43
97
  end
@@ -6,6 +6,77 @@
6
6
  module Compliance
7
7
  # Represents a policy for checking compliance.
8
8
  class Policy
9
+ class Error < StandardError
10
+ end
11
+
12
+ # Load the policy from a given loader.
13
+ def self.default(loader = Loader.default)
14
+ self.new.tap do |policy|
15
+ loader.documents.each do |document|
16
+ policy.add(document, loader)
17
+ end
18
+ end
19
+ end
20
+
21
+ def initialize
22
+ @requirements = {}
23
+ @attestations = {}
24
+ end
25
+
26
+ def as_json(...)
27
+ {
28
+ requirements: @requirements,
29
+ attestations: @attestations
30
+ }
31
+ end
32
+
33
+ def to_json(...)
34
+ as_json.to_json(...)
35
+ end
36
+
37
+ attr :requirements
38
+ attr :attestations
39
+
40
+ def add(document, loader)
41
+ document.requirements.each do |requirement|
42
+ if @requirements.key?(requirement.id)
43
+ raise Error.new("Duplicate requirement: #{requirement.id}")
44
+ end
45
+
46
+ @requirements[requirement.id] = requirement
47
+ end
48
+
49
+ document.attestations.each do |attestation|
50
+ (@attestations[attestation.id] ||= Array.new) << attestation
51
+ end
52
+
53
+ document.imports.each do |import|
54
+ import.resolve(self, loader)
55
+ end
56
+ end
57
+
58
+ # Check the document against a given policy.
59
+ def check
60
+ return to_enum(:check) unless block_given?
61
+
62
+ @requirements.each do |id, requirement|
63
+ attestations = @attestations[id]
64
+
65
+ satisfied = []
66
+ unsatisfied = []
67
+
68
+ attestations&.each do |attestation|
69
+ if self.satisfies?(requirement, attestation)
70
+ satisfied << attestation
71
+ else
72
+ unsatisfied << attestation
73
+ end
74
+ end
75
+
76
+ yield requirement, satisfied, unsatisfied
77
+ end
78
+ end
79
+
9
80
  def satisfies?(requirement, attestation)
10
81
  requirement.id == attestation.id
11
82
  end
@@ -25,5 +25,9 @@ module Compliance
25
25
 
26
26
  # The metadata associated with this requirement.
27
27
  attr :metadata
28
+
29
+ def [] key
30
+ @metadata[key]
31
+ end
28
32
  end
29
33
  end
@@ -4,5 +4,5 @@
4
4
  # Copyright, 2024, by Samuel Williams.
5
5
 
6
6
  module Compliance
7
- VERSION = "0.0.2"
7
+ VERSION = "0.1.0"
8
8
  end
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: compliance
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -37,7 +37,7 @@ cert_chain:
37
37
  Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
38
38
  voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
39
39
  -----END CERTIFICATE-----
40
- date: 2024-04-17 00:00:00.000000000 Z
40
+ date: 2024-04-18 00:00:00.000000000 Z
41
41
  dependencies:
42
42
  - !ruby/object:Gem::Dependency
43
43
  name: json
@@ -64,6 +64,7 @@ files:
64
64
  - lib/compliance/attestation.rb
65
65
  - lib/compliance/document.rb
66
66
  - lib/compliance/error.rb
67
+ - lib/compliance/import.rb
67
68
  - lib/compliance/loader.rb
68
69
  - lib/compliance/policy.rb
69
70
  - lib/compliance/requirement.rb
metadata.gz.sig CHANGED
Binary file