compliance 0.0.2 → 0.2.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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/bake/compliance/attest.rb +86 -0
- data/bake/compliance.rb +40 -15
- data/lib/compliance/attestation.rb +7 -0
- data/lib/compliance/document.rb +40 -42
- data/lib/compliance/error.rb +18 -1
- data/lib/compliance/import.rb +30 -0
- data/lib/compliance/loader.rb +76 -22
- data/lib/compliance/policy.rb +71 -0
- data/lib/compliance/requirement.rb +4 -0
- data/lib/compliance/version.rb +1 -1
- data.tar.gz.sig +0 -0
- metadata +4 -2
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2f821883f9be036f7d0c4b4b4c46a54cfb4d9c65f03d2a8e09a739d595d69cb4
|
4
|
+
data.tar.gz: e89f86fc987e18cae6643dd593f2388dd1f5822238eb4d72356735478626eb44
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d31ba046177f99b0bb31f11f5760e0b875dd4c1150c7cbca1a4bccca7bd3f07165a247a8fe24af28601e66632043fb5cca994baea85a14d98cb4f5d97b159371
|
7
|
+
data.tar.gz: 6502b0285c36ca163ff3e2f573a7072552bbae42047f54417b03688e2d2b60fd386ef548bfd8d03a98a904c27968d07d2d7e286b3bd2a6576372261370153faa
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2024, by Samuel Williams.
|
5
|
+
|
6
|
+
def initialize(...)
|
7
|
+
super
|
8
|
+
|
9
|
+
require 'compliance'
|
10
|
+
end
|
11
|
+
|
12
|
+
# Attest to a requirement.
|
13
|
+
# @parameter id [String] The unique identifier for the attestation, matching the requirement.
|
14
|
+
# @parameter description [String] A description of how the requirement is satisfied.
|
15
|
+
# @parameter by [String] The entity attesting to the requirement.
|
16
|
+
def attest(id, description: nil, by: nil)
|
17
|
+
compliance_root = Compliance::Document.path(context.root)
|
18
|
+
|
19
|
+
if File.exist?(compliance_root)
|
20
|
+
document = Compliance::Document.load(compliance_root)
|
21
|
+
else
|
22
|
+
document = Compliance::Document.new
|
23
|
+
end
|
24
|
+
|
25
|
+
attestation = self.attestation_for(id, document)
|
26
|
+
|
27
|
+
if description
|
28
|
+
attestation.metadata[:description] = description
|
29
|
+
end
|
30
|
+
|
31
|
+
if by
|
32
|
+
attestation.metadata[:by] = by
|
33
|
+
end
|
34
|
+
|
35
|
+
File.write(compliance_root, JSON.pretty_generate(document))
|
36
|
+
|
37
|
+
return attestation
|
38
|
+
end
|
39
|
+
|
40
|
+
def all(description: nil, by: nil)
|
41
|
+
compliance_root = Compliance::Document.path(context.root)
|
42
|
+
|
43
|
+
if File.exist?(compliance_root)
|
44
|
+
document = Compliance::Document.load(compliance_root)
|
45
|
+
else
|
46
|
+
document = Compliance::Document.new
|
47
|
+
end
|
48
|
+
|
49
|
+
policy = self.policy
|
50
|
+
|
51
|
+
policy.requirements.each do |id, requirement|
|
52
|
+
attestation = self.attestation_for(id, document)
|
53
|
+
|
54
|
+
if description
|
55
|
+
attestation.metadata[:description] = description
|
56
|
+
end
|
57
|
+
|
58
|
+
if by
|
59
|
+
attestation.metadata[:by] = by
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
File.write(compliance_root, JSON.pretty_generate(document))
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
# Load the default compliance policy.
|
69
|
+
def policy
|
70
|
+
loader = Compliance::Loader.default([context.root])
|
71
|
+
|
72
|
+
return Compliance::Policy.default(loader)
|
73
|
+
end
|
74
|
+
|
75
|
+
def attestation_for(id, document)
|
76
|
+
document.attestations.each do |attestation|
|
77
|
+
if attestation.id == id
|
78
|
+
return attestation
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
attestation = Compliance::Attestation.new(id: id)
|
83
|
+
document.attestations << attestation
|
84
|
+
|
85
|
+
return attestation
|
86
|
+
end
|
data/bake/compliance.rb
CHANGED
@@ -9,27 +9,47 @@ def initialize(...)
|
|
9
9
|
require 'compliance'
|
10
10
|
end
|
11
11
|
|
12
|
-
|
13
|
-
|
12
|
+
# Load the default compliance policy.
|
13
|
+
def policy
|
14
|
+
loader = Compliance::Loader.default([context.root])
|
14
15
|
|
15
|
-
::
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
end
|
16
|
+
return Compliance::Policy.default(loader)
|
17
|
+
end
|
18
|
+
|
19
|
+
# List available compliance documents.
|
20
|
+
def list
|
21
|
+
loader = Compliance::Loader.default([context.root])
|
22
22
|
|
23
|
-
|
23
|
+
loader.cache.map do |name, path|
|
24
|
+
{name: name, path: path}
|
25
|
+
end
|
24
26
|
end
|
25
27
|
|
26
|
-
|
27
|
-
|
28
|
-
|
28
|
+
# Show a specific requirement and any associated attestations.
|
29
|
+
# @parameter id [String] The unique identifier for the requirement.
|
30
|
+
def show(id)
|
31
|
+
policy = self.policy
|
32
|
+
|
33
|
+
requirement = policy.requirements[id]
|
29
34
|
|
35
|
+
if requirement
|
36
|
+
$stdout.puts "Requirement: #{requirement.id}"
|
37
|
+
$stdout.puts JSON.pretty_generate(requirement)
|
38
|
+
end
|
39
|
+
|
40
|
+
policy.attestations[id]&.each do |attestations|
|
41
|
+
$stdout.puts "Attestation: #{attestation.id}"
|
42
|
+
$stdout.puts JSON.pretty_generate(attestation)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Check compliance with the policy.
|
47
|
+
def check
|
48
|
+
policy = self.policy
|
49
|
+
|
30
50
|
failed_requirements = {}
|
31
51
|
|
32
|
-
|
52
|
+
results = policy.check do |requirement, satisfied, unsatisfied|
|
33
53
|
Console.debug(self) {"Requirement #{requirement.id} is #{satisfied.any? ? "satisfied." : "not satisfied!"}"}
|
34
54
|
|
35
55
|
if satisfied.empty?
|
@@ -37,5 +57,10 @@ def check(input:)
|
|
37
57
|
end
|
38
58
|
end
|
39
59
|
|
40
|
-
|
60
|
+
if failed_requirements.any?
|
61
|
+
raise Compliance::Error.new(failed_requirements)
|
62
|
+
else
|
63
|
+
Console.debug(self) {"All requirements are satisfied."}
|
64
|
+
return results
|
65
|
+
end
|
41
66
|
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
|
data/lib/compliance/document.rb
CHANGED
@@ -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
|
13
|
-
|
14
|
-
|
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
|
-
|
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
|
-
|
22
|
-
|
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
|
data/lib/compliance/error.rb
CHANGED
@@ -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 "
|
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
|
data/lib/compliance/loader.rb
CHANGED
@@ -10,34 +10,88 @@ require 'json'
|
|
10
10
|
|
11
11
|
module Compliance
|
12
12
|
# Load compliance data from JSON files.
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
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
|
-
|
20
|
-
|
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
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
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
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
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
|
data/lib/compliance/policy.rb
CHANGED
@@ -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
|
data/lib/compliance/version.rb
CHANGED
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
|
4
|
+
version: 0.2.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-
|
40
|
+
date: 2024-04-18 00:00:00.000000000 Z
|
41
41
|
dependencies:
|
42
42
|
- !ruby/object:Gem::Dependency
|
43
43
|
name: json
|
@@ -60,10 +60,12 @@ extensions: []
|
|
60
60
|
extra_rdoc_files: []
|
61
61
|
files:
|
62
62
|
- bake/compliance.rb
|
63
|
+
- bake/compliance/attest.rb
|
63
64
|
- lib/compliance.rb
|
64
65
|
- lib/compliance/attestation.rb
|
65
66
|
- lib/compliance/document.rb
|
66
67
|
- lib/compliance/error.rb
|
68
|
+
- lib/compliance/import.rb
|
67
69
|
- lib/compliance/loader.rb
|
68
70
|
- lib/compliance/policy.rb
|
69
71
|
- lib/compliance/requirement.rb
|
metadata.gz.sig
CHANGED
Binary file
|