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