compliance 0.0.2 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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