octool 0.0.3 → 0.0.8
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
- data/bin/octool +38 -11
- data/lib/octool.rb +1 -7
- data/lib/octool/constants.rb +1 -1
- data/lib/octool/parser.rb +42 -24
- data/lib/octool/ssp.rb +27 -17
- data/lib/octool/system.rb +41 -39
- data/lib/octool/version.rb +1 -1
- data/octool.rdoc +15 -3
- data/schemas/v1.0.1/certification.yaml +27 -0
- data/schemas/v1.0.1/component.yaml +60 -0
- data/schemas/v1.0.1/config.yaml +79 -0
- data/schemas/v1.0.1/standard.yaml +50 -0
- data/templates/ssp.erb +127 -37
- metadata +6 -6
- data/lib/octool/generated/certification.rb +0 -35
- data/lib/octool/generated/component.rb +0 -57
- data/lib/octool/generated/config.rb +0 -55
- data/lib/octool/generated/standard.rb +0 -55
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ed9a9d64af06f7c36bf6559eab5919e7b5488ec5a265830d757967dc60784b28
|
4
|
+
data.tar.gz: 0b71a16cd55225d995f0f1ff31c24517900726fe6e9a70a9ae54fa6b93b770fb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 338981986492cd44db6b36844f833865b5ed8966c332a74ed313d12650d00fe0f8d14dccd8db04bf0421adf305619a97bc3a5f17a0ff2ee400537c087f2d7895
|
7
|
+
data.tar.gz: 634d4943387eae05d72ab887c7e555fcb0c119ce5a0cec02ae855dc42cdba95f46a1a553c541b388e7c6e5f0edf141c3ade05870e6e510938426535f9e0ec2f5
|
data/bin/octool
CHANGED
@@ -8,6 +8,22 @@ require 'octool'
|
|
8
8
|
class App
|
9
9
|
extend GLI::App
|
10
10
|
|
11
|
+
def self.data_dir
|
12
|
+
[ENV['TMPDIR'], ENV['TMP'], ENV['TEMP'], '/tmp'].each do |dir|
|
13
|
+
next if dir.nil?
|
14
|
+
|
15
|
+
stat = File.stat(dir)
|
16
|
+
return dir if stat.directory? && stat.writable?
|
17
|
+
end
|
18
|
+
OCTool::DEFAULT_OUTPUT_DIR
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.find_config(args)
|
22
|
+
path = args.first || OCTool::DEFAULT_CONFIG_FILENAME
|
23
|
+
path = File.join(path, OCTool::DEFAULT_CONFIG_FILENAME) if File.directory?(path)
|
24
|
+
File.expand_path(path)
|
25
|
+
end
|
26
|
+
|
11
27
|
program_desc 'Open Compliance Tool'
|
12
28
|
version OCTool::VERSION
|
13
29
|
|
@@ -35,11 +51,28 @@ class App
|
|
35
51
|
v.default_command :data
|
36
52
|
end
|
37
53
|
|
38
|
-
desc '
|
54
|
+
desc 'export data to CSV'
|
55
|
+
arg_name 'path/to/system/config.yaml'
|
56
|
+
command :csv do |csv|
|
57
|
+
csv.desc 'where to store outputs'
|
58
|
+
csv.default_value data_dir
|
59
|
+
csv.long_desc 'Default output directory respects env vars TMPDIR, TMP, TEMP'
|
60
|
+
csv.arg_name 'path/to/output/dir'
|
61
|
+
csv.flag [:d, :dir]
|
62
|
+
|
63
|
+
csv.action do |global_options, options, args|
|
64
|
+
export_dir = options[:dir]
|
65
|
+
config_file = find_config(args)
|
66
|
+
system = OCTool::Parser.new(config_file).load_system
|
67
|
+
system.dump export_dir
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
desc 'validate data and generate System Security Plan'
|
39
72
|
arg_name 'path/to/system/config.yaml'
|
40
73
|
command :ssp do |s|
|
41
74
|
s.desc 'where to store outputs'
|
42
|
-
s.default_value
|
75
|
+
s.default_value data_dir
|
43
76
|
s.long_desc 'Default output directory respects env vars TMPDIR, TMP, TEMP'
|
44
77
|
s.arg_name 'path/to/output/dir'
|
45
78
|
s.flag [:d, :dir]
|
@@ -76,20 +109,14 @@ class App
|
|
76
109
|
# Error logic here
|
77
110
|
# Return false to skip default error handling.
|
78
111
|
if ENV['DEBUG']
|
79
|
-
|
80
|
-
|
112
|
+
warn exception.message
|
113
|
+
warn exception.backtrace
|
81
114
|
pp exception
|
82
115
|
false
|
83
116
|
end
|
84
|
-
|
117
|
+
warn '[FAIL]'
|
85
118
|
true
|
86
119
|
end
|
87
120
|
end
|
88
121
|
|
89
|
-
def find_config(args)
|
90
|
-
path = args.first || OCTool::DEFAULT_CONFIG_FILENAME
|
91
|
-
path = File.join(path, OCTool::DEFAULT_CONFIG_FILENAME) if File.directory?(path)
|
92
|
-
path
|
93
|
-
end
|
94
|
-
|
95
122
|
exit App.run(ARGV)
|
data/lib/octool.rb
CHANGED
@@ -3,8 +3,8 @@
|
|
3
3
|
require 'octool/version.rb'
|
4
4
|
|
5
5
|
# Built-ins.
|
6
|
+
require 'csv'
|
6
7
|
require 'pp'
|
7
|
-
require 'tmpdir'
|
8
8
|
|
9
9
|
# 3rd-party libs.
|
10
10
|
require 'kwalify'
|
@@ -16,12 +16,6 @@ require 'octool/parser'
|
|
16
16
|
require 'octool/ssp'
|
17
17
|
require 'octool/system'
|
18
18
|
|
19
|
-
# Generated libs.
|
20
|
-
require 'octool/generated/certification'
|
21
|
-
require 'octool/generated/component'
|
22
|
-
require 'octool/generated/config'
|
23
|
-
require 'octool/generated/standard'
|
24
|
-
|
25
19
|
# Mixins.
|
26
20
|
module OCTool
|
27
21
|
include Kwalify::Util::HashLike # defines [], []=, and keys?
|
data/lib/octool/constants.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module OCTool
|
4
|
-
LATEST_SCHEMA_VERSION = 'v1.0.
|
4
|
+
LATEST_SCHEMA_VERSION = 'v1.0.1'
|
5
5
|
BASE_SCHEMA_DIR = File.join(File.dirname(__FILE__), '..', '..', 'schemas').freeze
|
6
6
|
ERB_DIR = File.join(File.dirname(__FILE__), '..', '..', 'templates').freeze
|
7
7
|
DEFAULT_CONFIG_FILENAME = 'config.yaml'
|
data/lib/octool/parser.rb
CHANGED
@@ -4,6 +4,7 @@ module OCTool
|
|
4
4
|
# Custom error to show validation errors.
|
5
5
|
class ValidationError < StandardError
|
6
6
|
attr_reader :errors
|
7
|
+
|
7
8
|
def initialize(path, errors)
|
8
9
|
@path = path
|
9
10
|
@errors = errors
|
@@ -40,9 +41,9 @@ module OCTool
|
|
40
41
|
end
|
41
42
|
|
42
43
|
def validate_file(path, type)
|
43
|
-
|
44
|
-
data =
|
45
|
-
errors =
|
44
|
+
kwalify = kwalifyer(type)
|
45
|
+
data = kwalify.parse_file(path)
|
46
|
+
errors = kwalify.errors
|
46
47
|
raise ValidationError.new(path, errors) unless errors.empty?
|
47
48
|
|
48
49
|
data
|
@@ -50,7 +51,7 @@ module OCTool
|
|
50
51
|
die e.message
|
51
52
|
end
|
52
53
|
|
53
|
-
def
|
54
|
+
def kwalifyer(type)
|
54
55
|
schema_file = File.join(schema_dir, "#{type}.yaml")
|
55
56
|
schema = Kwalify::Yaml.load_file(schema_file)
|
56
57
|
validator = Kwalify::Validator.new(schema)
|
@@ -64,40 +65,57 @@ module OCTool
|
|
64
65
|
def schema_version
|
65
66
|
@schema_version ||= Kwalify::Yaml.load_file(@config_file)['schema_version']
|
66
67
|
rescue StandarError
|
67
|
-
|
68
|
+
warn '[FAIL] Unable to read schema_version'
|
68
69
|
exit(1)
|
69
70
|
end
|
70
71
|
|
71
|
-
#
|
72
|
+
# Validate and load data in one pass.
|
72
73
|
def validate_data
|
73
74
|
base_dir = File.dirname(@config_file)
|
74
75
|
config = validate_file(@config_file, 'config')
|
76
|
+
sys = System.new(config)
|
75
77
|
config['includes'].each do |inc|
|
76
78
|
path = File.join(base_dir, inc['path'])
|
77
|
-
|
78
|
-
validate_file(path, type)
|
79
|
+
sys.data << include_data(path, inc['type'])
|
79
80
|
end
|
80
|
-
|
81
|
+
sys
|
81
82
|
end
|
82
83
|
|
83
|
-
def
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
84
|
+
def include_data(path, type)
|
85
|
+
data = validate_file(path, type)
|
86
|
+
data['type'] = type
|
87
|
+
method("parsed_#{type}".to_sym).call(data)
|
88
|
+
end
|
89
|
+
|
90
|
+
def parsed_component(component)
|
91
|
+
component['attestations'].map! do |a|
|
92
|
+
# Add a "component_key" field to each attestation.
|
93
|
+
a['component_key'] = component['component_key']
|
94
|
+
a['satisfies'].map! do |s|
|
95
|
+
# Add "attestation_key" to each control satisfied by this attestation.
|
96
|
+
s['attestation_key'] = a['summary']
|
97
|
+
# Add "component_key" to each control satisfied by this attestation.
|
98
|
+
s['component_key'] = component['component_key']
|
99
|
+
s
|
100
|
+
end
|
101
|
+
a
|
90
102
|
end
|
91
|
-
|
103
|
+
component
|
92
104
|
end
|
93
105
|
|
94
|
-
def
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
106
|
+
def parsed_standard(standard)
|
107
|
+
# Add 'standard_key' to each control family and to each control.
|
108
|
+
standard['families'].map! { |f| f['standard_key'] = standard['standard_key']; f }
|
109
|
+
standard['controls'].map! { |c| c['standard_key'] = standard['standard_key']; c }
|
110
|
+
standard
|
111
|
+
end
|
112
|
+
|
113
|
+
def parsed_certification(cert)
|
114
|
+
cert['requires'].map! { |r| r['certification_key'] = cert['certification_key']; r }
|
115
|
+
cert
|
101
116
|
end
|
117
|
+
|
118
|
+
alias load_system validate_data
|
119
|
+
alias load_file validate_file
|
102
120
|
end
|
103
121
|
end
|
data/lib/octool/ssp.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'erb'
|
2
4
|
|
3
5
|
module OCTool
|
@@ -6,21 +8,28 @@ module OCTool
|
|
6
8
|
def initialize(system, output_dir)
|
7
9
|
@system = system
|
8
10
|
@output_dir = output_dir
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
11
|
+
end
|
12
|
+
|
13
|
+
def pandoc
|
14
|
+
@pandoc ||= begin
|
15
|
+
# Load paru/pandoc late enough that help functions work
|
16
|
+
# and early enough to be confident that we catch the correct error.
|
17
|
+
require 'paru/pandoc'
|
18
|
+
Paru::Pandoc.new
|
19
|
+
end
|
20
|
+
rescue UncaughtThrowError
|
21
|
+
warn '[FAIL] octool requires pandoc to generate the SSP. Is pandoc installed?'
|
14
22
|
exit(1)
|
15
23
|
end
|
16
24
|
|
17
25
|
def generate
|
18
|
-
|
19
|
-
|
26
|
+
unless File.writable?(@output_dir)
|
27
|
+
warn "[FAIL] #{@output_dir} is not writable"
|
20
28
|
exit(1)
|
21
29
|
end
|
22
30
|
render_template
|
23
|
-
write
|
31
|
+
write 'pdf'
|
32
|
+
write 'docx'
|
24
33
|
end
|
25
34
|
|
26
35
|
def render_template
|
@@ -32,29 +41,30 @@ module OCTool
|
|
32
41
|
puts 'done'
|
33
42
|
end
|
34
43
|
|
35
|
-
|
36
|
-
|
37
|
-
|
44
|
+
# rubocop:disable Metrics/AbcSize,Metrics/MethodLength
|
45
|
+
def write(type = 'pdf')
|
46
|
+
out_path = File.join(@output_dir, "ssp.#{type}")
|
47
|
+
print "Building #{out_path} ... "
|
38
48
|
converter = pandoc.configure do
|
39
49
|
from 'markdown'
|
40
|
-
to
|
50
|
+
to type
|
41
51
|
pdf_engine 'lualatex'
|
42
52
|
toc
|
43
53
|
toc_depth 3
|
44
54
|
number_sections
|
45
55
|
highlight_style 'pygments'
|
56
|
+
# https://en.wikibooks.org/wiki/LaTeX/Source_Code_Listings#Encoding_issue
|
57
|
+
# Uncomment the following line after the "listings" package is compatible with utf8
|
58
|
+
# listings
|
46
59
|
end
|
47
60
|
output = converter << File.read(md_path)
|
48
|
-
File.new(
|
61
|
+
File.new(out_path, 'wb').write(output)
|
49
62
|
puts 'done'
|
50
63
|
end
|
64
|
+
# rubocop:enable Metrics/AbcSize,Metrics/MethodLength
|
51
65
|
|
52
66
|
def md_path
|
53
67
|
@md_path ||= File.join(@output_dir, 'ssp.md')
|
54
68
|
end
|
55
|
-
|
56
|
-
def pdf_path
|
57
|
-
@pdf_path ||= File.join(@output_dir, 'ssp.pdf')
|
58
|
-
end
|
59
69
|
end
|
60
70
|
end
|
data/lib/octool/system.rb
CHANGED
@@ -6,73 +6,75 @@ module OCTool
|
|
6
6
|
attr_accessor :config
|
7
7
|
attr_accessor :data
|
8
8
|
|
9
|
+
TABLE_NAMES = [
|
10
|
+
'components',
|
11
|
+
'satisfies',
|
12
|
+
'attestations',
|
13
|
+
'standards',
|
14
|
+
'controls',
|
15
|
+
'families',
|
16
|
+
'certifications',
|
17
|
+
'requires',
|
18
|
+
].freeze
|
19
|
+
|
9
20
|
def initialize(config)
|
10
21
|
@config = config
|
11
22
|
@data = []
|
12
23
|
end
|
13
24
|
|
14
25
|
def certifications
|
15
|
-
@certifications ||=
|
26
|
+
@certifications ||= data.select { |e| e['type'] == 'certification' }
|
16
27
|
end
|
17
28
|
|
18
29
|
def components
|
19
|
-
@components ||=
|
30
|
+
@components ||= data.select { |e| e['type'] == 'component' }
|
20
31
|
end
|
21
32
|
|
22
33
|
def standards
|
23
|
-
@standards ||=
|
34
|
+
@standards ||= data.select { |e| e['type'] == 'standard' }
|
24
35
|
end
|
25
36
|
|
26
37
|
# List of all attestations claimed by components in the system.
|
27
38
|
def attestations
|
28
|
-
@attestations ||=
|
29
|
-
@attestations = []
|
30
|
-
components.each do |c|
|
31
|
-
# Add a "component_key" field to each attestation.
|
32
|
-
c.attestations.map! { |e| e['component_key'] = c.component_key; e }
|
33
|
-
@attestations << c.attestations
|
34
|
-
end
|
35
|
-
@attestations.flatten!
|
36
|
-
end
|
39
|
+
@attestations ||= components.map { |c| c['attestations'] }.flatten
|
37
40
|
end
|
38
41
|
|
39
42
|
# List of all coverages.
|
40
43
|
def satisfies
|
41
|
-
@satisfies ||=
|
42
|
-
@satisfies = []
|
43
|
-
attestations.each do |a|
|
44
|
-
# Add an "attestation_key" field to each cover.
|
45
|
-
a.satisfies.map! { |e| e['component_key'] = a.commponent_key; e }
|
46
|
-
a.satisfies.map! { |e| e['attestation_key'] = a.attestation_summary; e }
|
47
|
-
@satisfies << a.satisfies
|
48
|
-
end
|
49
|
-
@satisfies.flatten!
|
50
|
-
end
|
44
|
+
@satisfies ||= attestations.map { |a| a['satisfies'] }.flatten
|
51
45
|
end
|
52
46
|
|
53
47
|
# List of all controls defined by standards in the system.
|
54
48
|
def controls
|
55
|
-
@controls
|
56
|
-
@controls = []
|
57
|
-
standards.each do |s|
|
58
|
-
# Add a "standard_key" field to each control.
|
59
|
-
s.controls.map! { |e| e['standard_key'] = s.standard_key; e }
|
60
|
-
@controls << s.controls
|
61
|
-
end
|
62
|
-
@controls.flatten!
|
63
|
-
end
|
49
|
+
@controls ||= standards.map { |s| s['controls'] }.flatten
|
64
50
|
end
|
65
51
|
|
66
52
|
# List of all families defined by standards in the system.
|
67
53
|
def families
|
68
|
-
@families
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
54
|
+
@families ||= standards.map { |s| s['families'] }.flatten
|
55
|
+
end
|
56
|
+
|
57
|
+
# List of required controls for all certifications.
|
58
|
+
def requires
|
59
|
+
@requires ||= certifications.map { |c| c['requires'] }.flatten
|
60
|
+
end
|
61
|
+
|
62
|
+
def dump(writable_dir)
|
63
|
+
TABLE_NAMES.each do |table|
|
64
|
+
write_csv method(table.to_sym).call, File.join(writable_dir, "#{table}.csv")
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Convert array of hashes into a CSV.
|
69
|
+
def write_csv(ary, filename)
|
70
|
+
# Throw away nested hashes. The parser already created separate tables for them.
|
71
|
+
ary = ary.map { |e| e.reject { |_, val| val.is_a?(Enumerable) } }
|
72
|
+
|
73
|
+
warn "[INFO] write #{filename}"
|
74
|
+
CSV.open(filename, 'wb') do |csv|
|
75
|
+
column_names = ary.first.keys
|
76
|
+
csv << column_names
|
77
|
+
ary.each { |hash| csv << hash.values_at(*column_names) }
|
76
78
|
end
|
77
79
|
end
|
78
80
|
end
|
data/lib/octool/version.rb
CHANGED
data/octool.rdoc
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
== octool - Open Compliance Tool
|
2
2
|
|
3
|
-
v0.0.
|
3
|
+
v0.0.8
|
4
4
|
|
5
5
|
=== Global Options
|
6
6
|
=== --help
|
@@ -14,6 +14,18 @@ Display the program version
|
|
14
14
|
|
15
15
|
|
16
16
|
=== Commands
|
17
|
+
==== Command: <tt>csv path/to/system/config.yaml</tt>
|
18
|
+
export data to CSV
|
19
|
+
|
20
|
+
|
21
|
+
===== Options
|
22
|
+
===== -d|--dir path/to/output/dir
|
23
|
+
|
24
|
+
where to store outputs
|
25
|
+
|
26
|
+
[Default Value] /tmp
|
27
|
+
Default output directory respects env vars TMPDIR, TMP, TEMP
|
28
|
+
|
17
29
|
==== Command: <tt>help command</tt>
|
18
30
|
Shows a list of commands or help for one command
|
19
31
|
|
@@ -25,7 +37,7 @@ List commands one per line, to assist with shell completion
|
|
25
37
|
|
26
38
|
|
27
39
|
==== Command: <tt>ssp path/to/system/config.yaml</tt>
|
28
|
-
generate System Security Plan
|
40
|
+
validate data and generate System Security Plan
|
29
41
|
|
30
42
|
|
31
43
|
===== Options
|
@@ -33,7 +45,7 @@ generate System Security Plan
|
|
33
45
|
|
34
46
|
where to store outputs
|
35
47
|
|
36
|
-
[Default Value] /
|
48
|
+
[Default Value] /tmp
|
37
49
|
Default output directory respects env vars TMPDIR, TMP, TEMP
|
38
50
|
|
39
51
|
==== Command: <tt>validate </tt>
|
@@ -0,0 +1,27 @@
|
|
1
|
+
---
|
2
|
+
type: map
|
3
|
+
class: Certification
|
4
|
+
mapping:
|
5
|
+
certification_key:
|
6
|
+
desc: A short, unique identifier for this certification.
|
7
|
+
required: true
|
8
|
+
type: str
|
9
|
+
unique: true
|
10
|
+
name:
|
11
|
+
desc: A human-friendly name for the certification.
|
12
|
+
required: true
|
13
|
+
type: str
|
14
|
+
requires:
|
15
|
+
desc: List of control IDs required by the certification.
|
16
|
+
required: true
|
17
|
+
type: seq
|
18
|
+
sequence:
|
19
|
+
- type: map
|
20
|
+
class: ControlID
|
21
|
+
mapping:
|
22
|
+
standard_key:
|
23
|
+
required: true
|
24
|
+
type: str
|
25
|
+
control_key:
|
26
|
+
required: true
|
27
|
+
type: str
|
@@ -0,0 +1,60 @@
|
|
1
|
+
---
|
2
|
+
type: map
|
3
|
+
class: Component
|
4
|
+
mapping:
|
5
|
+
name:
|
6
|
+
desc: Human-friendly name to appear in the SSP.
|
7
|
+
type: str
|
8
|
+
required: true
|
9
|
+
component_key:
|
10
|
+
desc: Unique identifier for referential integrity.
|
11
|
+
type: str
|
12
|
+
required: true
|
13
|
+
description:
|
14
|
+
desc: A paragraph or two that describes the component.
|
15
|
+
type: str
|
16
|
+
required: true
|
17
|
+
attestations:
|
18
|
+
desc: List of attestations.
|
19
|
+
type: seq
|
20
|
+
sequence:
|
21
|
+
- type: map
|
22
|
+
class: Attestation
|
23
|
+
mapping:
|
24
|
+
summary:
|
25
|
+
desc: Arbitrary verbiage to appear in SSP as a TLDR.
|
26
|
+
type: str
|
27
|
+
required: true
|
28
|
+
status:
|
29
|
+
desc: To what extent is this attestation "done"?
|
30
|
+
type: str
|
31
|
+
required: true
|
32
|
+
enum:
|
33
|
+
- partial
|
34
|
+
- complete
|
35
|
+
- planned
|
36
|
+
- none
|
37
|
+
date_verified:
|
38
|
+
desc: When was this last verified?
|
39
|
+
type: date
|
40
|
+
required: false
|
41
|
+
satisfies:
|
42
|
+
desc: List of control IDs covered by this attestation.
|
43
|
+
type: seq
|
44
|
+
required: false
|
45
|
+
sequence:
|
46
|
+
- type: map
|
47
|
+
class: ControlID
|
48
|
+
mapping:
|
49
|
+
standard_key:
|
50
|
+
type: text
|
51
|
+
required: true
|
52
|
+
control_key:
|
53
|
+
type: text
|
54
|
+
required: true
|
55
|
+
narrative:
|
56
|
+
desc: |
|
57
|
+
Explain how attestation satisfies the indicated controls.
|
58
|
+
The content should be in markdown format.
|
59
|
+
type: str
|
60
|
+
required: true
|
@@ -0,0 +1,79 @@
|
|
1
|
+
---
|
2
|
+
type: map
|
3
|
+
class: Config
|
4
|
+
mapping:
|
5
|
+
schema_version:
|
6
|
+
desc: |
|
7
|
+
Must match one of the schema directories in the octool source.
|
8
|
+
required: true
|
9
|
+
type: str
|
10
|
+
|
11
|
+
logo:
|
12
|
+
desc: Image for title page.
|
13
|
+
required: false
|
14
|
+
type: map
|
15
|
+
class: Logo
|
16
|
+
mapping:
|
17
|
+
path:
|
18
|
+
desc: Path to image.
|
19
|
+
type: str
|
20
|
+
required: true
|
21
|
+
width:
|
22
|
+
desc: Width of image, such as "1in" or "254mm"
|
23
|
+
type: str
|
24
|
+
required: true
|
25
|
+
|
26
|
+
name:
|
27
|
+
desc: Human-friendly to appear in the SSP.
|
28
|
+
required: true
|
29
|
+
type: str
|
30
|
+
|
31
|
+
overview:
|
32
|
+
desc: Human-friendly description to appear in the SSP.
|
33
|
+
required: true
|
34
|
+
type: str
|
35
|
+
|
36
|
+
maintainers:
|
37
|
+
desc: Who should somebody contact for questions about this SSP?
|
38
|
+
required: true
|
39
|
+
type: seq
|
40
|
+
sequence:
|
41
|
+
- type: str
|
42
|
+
|
43
|
+
metadata:
|
44
|
+
desc: Optional metadata.
|
45
|
+
required: false
|
46
|
+
type: map
|
47
|
+
class: Metadata
|
48
|
+
mapping:
|
49
|
+
abstract:
|
50
|
+
desc: Abstract appears in document metadata.
|
51
|
+
required: false
|
52
|
+
type: str
|
53
|
+
description:
|
54
|
+
desc: Description appears in document metadata.
|
55
|
+
required: false
|
56
|
+
type: str
|
57
|
+
'=':
|
58
|
+
desc: Arbitrary key:value pair of strings.
|
59
|
+
type: str
|
60
|
+
|
61
|
+
includes:
|
62
|
+
desc: Additional files to include from the system repo.
|
63
|
+
required: true
|
64
|
+
type: seq
|
65
|
+
sequence:
|
66
|
+
- type: map
|
67
|
+
class: Include
|
68
|
+
mapping:
|
69
|
+
type:
|
70
|
+
required: true
|
71
|
+
type: str
|
72
|
+
enum:
|
73
|
+
- certification
|
74
|
+
- component
|
75
|
+
- standard
|
76
|
+
path:
|
77
|
+
desc: Path must be relative within the repo.
|
78
|
+
required: true
|
79
|
+
type: str
|
@@ -0,0 +1,50 @@
|
|
1
|
+
---
|
2
|
+
type: map
|
3
|
+
class: Standard
|
4
|
+
mapping:
|
5
|
+
name:
|
6
|
+
desc: Human-friendly name to appear in SSP.
|
7
|
+
type: str
|
8
|
+
required: true
|
9
|
+
|
10
|
+
standard_key:
|
11
|
+
desc: Unique ID to use within YAML files.
|
12
|
+
type: str
|
13
|
+
required: true
|
14
|
+
|
15
|
+
families:
|
16
|
+
desc: Optional list of control families.
|
17
|
+
type: seq
|
18
|
+
required: false
|
19
|
+
sequence:
|
20
|
+
- type: map
|
21
|
+
class: ControlFamily
|
22
|
+
mapping:
|
23
|
+
family_key:
|
24
|
+
desc: Unique ID of the family
|
25
|
+
type: str
|
26
|
+
unique: true
|
27
|
+
name:
|
28
|
+
desc: Human-friendly name of the family
|
29
|
+
type: str
|
30
|
+
controls:
|
31
|
+
desc: Mandatory list of controls defined by the standard.
|
32
|
+
required: true
|
33
|
+
type: seq
|
34
|
+
sequence:
|
35
|
+
- type: map
|
36
|
+
class: Control
|
37
|
+
mapping:
|
38
|
+
control_key:
|
39
|
+
type: str
|
40
|
+
unique: true
|
41
|
+
required: true
|
42
|
+
family_key:
|
43
|
+
type: str
|
44
|
+
required: false
|
45
|
+
name:
|
46
|
+
type: str
|
47
|
+
required: true
|
48
|
+
description:
|
49
|
+
type: str
|
50
|
+
required: true
|
data/templates/ssp.erb
CHANGED
@@ -1,17 +1,25 @@
|
|
1
1
|
---
|
2
|
-
|
2
|
+
<% if @system.config['logo'] -%>
|
3
|
+
title: |
|
4
|
+
{width=<%= @system.config['logo']['width'] %>}
|
5
|
+
|
6
|
+
<%= @system.config['name'] %>
|
7
|
+
<% else %>
|
8
|
+
title: "<%= @system.config['name'] -%>"
|
9
|
+
<% end %>
|
10
|
+
|
3
11
|
subtitle: "System Security Plan"
|
4
12
|
|
5
13
|
author:
|
6
|
-
<% @system.config
|
14
|
+
<% @system.config['maintainers'].each do |maintainer| %>
|
7
15
|
- <%= maintainer -%>
|
8
16
|
<% end %>
|
9
17
|
|
10
18
|
absract: |
|
11
|
-
<%= @system.config
|
19
|
+
<%= @system.config['metadata']['abstract'] rescue 'None' %>
|
12
20
|
|
13
21
|
description: |
|
14
|
-
<%= @system.config
|
22
|
+
<%= @system.config['metadata']['description'] rescue 'None' %>
|
15
23
|
|
16
24
|
fontsize: 11pt
|
17
25
|
mainfont: NotoSans
|
@@ -25,10 +33,10 @@ mainfontoptions:
|
|
25
33
|
- BoldFont=*-Bold
|
26
34
|
- BoldItalicFont=*-BoldItalic
|
27
35
|
|
28
|
-
lof:
|
29
|
-
lot:
|
36
|
+
lof: true
|
37
|
+
lot: true
|
30
38
|
colorlinks: true
|
31
|
-
linkcolor:
|
39
|
+
linkcolor: black # internal links (e.g., lof and lot)
|
32
40
|
urlcolor: blue
|
33
41
|
|
34
42
|
documentclass: report
|
@@ -44,52 +52,128 @@ geometry:
|
|
44
52
|
- left=2cm
|
45
53
|
- right=2cm
|
46
54
|
- bottom=2cm
|
55
|
+
|
56
|
+
header-includes:
|
57
|
+
- |
|
58
|
+
```{=latex}
|
59
|
+
% https://github.com/jgm/pandoc/wiki/Pandoc-Tricks#left-aligning-tables-in-latex
|
60
|
+
\usepackage[margins=raggedright]{floatrow}
|
61
|
+
```
|
62
|
+
- |
|
63
|
+
```{=latex}
|
64
|
+
% https://github.com/jgm/pandoc/wiki/Pandoc-Tricks#definition-list-terms-on-their-own-line-in-latex
|
65
|
+
% "Clone" the original \item command
|
66
|
+
\let\originalitem\item
|
67
|
+
|
68
|
+
% Redefine the \item command using the "clone"
|
69
|
+
\makeatletter
|
70
|
+
\renewcommand{\item}[1][\@nil]{%
|
71
|
+
\def\tmp{#1}%
|
72
|
+
\ifx\tmp\@nnil\originalitem\else\originalitem[#1]\hfill\par\fi}
|
73
|
+
\makeatother
|
74
|
+
```
|
75
|
+
- |
|
76
|
+
```{=latex}
|
77
|
+
% The are at least two ways to configure how LaTeX floats figures.
|
78
|
+
%
|
79
|
+
% 1. One approach is described in section 17.2 of
|
80
|
+
% http://tug.ctan.org/tex-archive/info/epslatex/english/epslatex.pdf
|
81
|
+
% However, the approach described there requires to teach people
|
82
|
+
% how to write LaTeX cross-references in markdown.
|
83
|
+
%
|
84
|
+
% 2. Force figures, listings, etc., to float "[H]ere".
|
85
|
+
% This is a LaTeX anti-pattern because it causes large gaps of whitespace on some pages.
|
86
|
+
% This approach avoids having to teach people to create LaTeX cross-references.
|
87
|
+
% https://tex.stackexchange.com/a/101726
|
88
|
+
%
|
89
|
+
% Use option 2.
|
90
|
+
\usepackage{float}
|
91
|
+
\floatplacement{figure}{H}
|
92
|
+
```
|
47
93
|
---
|
48
94
|
|
49
|
-
#
|
95
|
+
# Introduction
|
50
96
|
|
51
|
-
##
|
97
|
+
## About this document
|
98
|
+
|
99
|
+
A System Security Plan (SSP) is a document to describe security controls in use
|
100
|
+
on an information system and their implementation. An SSP provides:
|
101
|
+
|
102
|
+
- Narrative of security control implementation
|
103
|
+
- Description of components and services
|
104
|
+
- System data flows and authorization boundaries
|
52
105
|
|
53
|
-
<%= @system.config.overview %>
|
54
106
|
|
55
107
|
## Standards
|
56
108
|
|
57
|
-
This
|
109
|
+
This SSP draws from these standards:
|
58
110
|
|
59
111
|
<% @system.standards.each do |s| -%>
|
60
|
-
- <%= s
|
112
|
+
- <%= s['name'] %>
|
61
113
|
<% end %>
|
62
114
|
|
63
115
|
The full copy of each standard is included in the appendix.
|
64
116
|
|
65
117
|
|
66
|
-
##
|
118
|
+
## Certifications
|
67
119
|
|
68
|
-
|
69
|
-
|
120
|
+
A certification is a logical grouping of controls that are of interest to
|
121
|
+
a given subject. A particular certification does not necessarily target all
|
122
|
+
controls from a standard, nor does a particular certification need to draw
|
123
|
+
from a single standard.
|
70
124
|
|
71
|
-
|
125
|
+
This SSP addresses these certifications:
|
126
|
+
|
127
|
+
<% @system.certifications.each do |c| -%>
|
128
|
+
- <%=c['name']%>
|
129
|
+
|
130
|
+
<% c['requires'].each do |r| -%>
|
131
|
+
- <%=r['standard_key']-%> control <%=r['control_key']%>
|
132
|
+
<% end -%>
|
72
133
|
|
73
|
-
<% if c.attestations.empty? %>
|
74
|
-
_The organization has not yet documented attestations for this component_.
|
75
|
-
<% else %>
|
76
|
-
The organization offers the following attestations for this component.
|
77
134
|
<% end %>
|
78
135
|
|
79
|
-
<% c.attestations.each do |a| %>
|
80
|
-
#### <%= a.summary %>
|
81
136
|
|
82
|
-
|
137
|
+
# <%= @system.config['name'] %>
|
83
138
|
|
84
|
-
|
139
|
+
## Overview
|
85
140
|
|
86
|
-
|
141
|
+
<%= @system.config['overview'] %>
|
87
142
|
|
88
|
-
<% a.satisfies.each do |cid| -%>
|
89
|
-
- <%= cid.standard_key %> control <%= cid.control_key %>
|
90
|
-
<% end -%>
|
91
143
|
|
92
|
-
|
144
|
+
## Components
|
145
|
+
|
146
|
+
<% @system.components.each do |c| %>
|
147
|
+
### <%= c['name'] %>
|
148
|
+
|
149
|
+
<%= c['description'] %>
|
150
|
+
|
151
|
+
<% if c['attestations'].empty? %>
|
152
|
+
_The organization has not yet documented attestations for this component_.
|
153
|
+
<% else %>
|
154
|
+
The organization offers the following attestations for this component.
|
155
|
+
<% end %>
|
156
|
+
|
157
|
+
<% c['attestations'].compact.each do |a| %>
|
158
|
+
#### <%= a['summary'] %>
|
159
|
+
|
160
|
+
+----------+---------------+--------------------------------------------------------------+
|
161
|
+
| Status | Date verified | Satisfies |
|
162
|
+
+==========+===============+==============================================================+
|
163
|
+
<%
|
164
|
+
s = a['satisfies'][0]
|
165
|
+
verbiage = sprintf('%-58s', [s['standard_key'], 'control', s['control_key']].join(' '))
|
166
|
+
-%>
|
167
|
+
| <%=sprintf('%-8s', a['status'])-%> | <%=sprintf('%-13s', a['date_verified'])-%> | - <%=verbiage-%> |
|
168
|
+
<%
|
169
|
+
a['satisfies'][1..].each do |s|
|
170
|
+
verbiage = sprintf('%-58s', [s['standard_key'], 'control', s['control_key']].join(' '))
|
171
|
+
-%>
|
172
|
+
| | | - <%=verbiage-%> |
|
173
|
+
<% end -%>
|
174
|
+
+----------+---------------+--------------------------------------------------------------+
|
175
|
+
|
176
|
+
<%= a['narrative'] %>
|
93
177
|
|
94
178
|
<% end %>
|
95
179
|
<% end %>
|
@@ -98,23 +182,29 @@ Satisfies:
|
|
98
182
|
# Appendix: Standards
|
99
183
|
|
100
184
|
<% @system.standards.each do |s| %>
|
101
|
-
## <%=s
|
185
|
+
## <%=s['name'] %>
|
102
186
|
|
103
|
-
<% if s
|
187
|
+
<% if s['families'] and !s['families'].empty? %>
|
104
188
|
### Families
|
105
189
|
|
106
|
-
|
107
|
-
|
108
|
-
|
190
|
+
<%=s['name']-%> categorizes controls into logical groups called families.
|
191
|
+
|
192
|
+
| Family abbreviation | Family name |
|
193
|
+
| -------------------------- | -------------------- |
|
194
|
+
<% s['families'].each do |family| -%>
|
195
|
+
| <%=family['family_key']-%> | <%=family['name']-%> |
|
196
|
+
<% end -%>
|
197
|
+
|
198
|
+
: Control families for <%=s['name']%>
|
109
199
|
|
110
200
|
<% end %>
|
111
201
|
|
112
202
|
### Controls
|
113
203
|
|
114
|
-
<% s
|
115
|
-
#### Control <%= c
|
204
|
+
<% s['controls'].each do |c| %>
|
205
|
+
#### Control <%= c['control_key'] -%>: <%= c['name'] %>
|
116
206
|
|
117
|
-
<%= c
|
207
|
+
<%= c['description'] %>
|
118
208
|
|
119
209
|
<% end %>
|
120
210
|
<% end %>
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: octool
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.8
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Paul Morgan
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-05-
|
11
|
+
date: 2020-05-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake
|
@@ -147,10 +147,6 @@ files:
|
|
147
147
|
- bin/octool
|
148
148
|
- lib/octool.rb
|
149
149
|
- lib/octool/constants.rb
|
150
|
-
- lib/octool/generated/certification.rb
|
151
|
-
- lib/octool/generated/component.rb
|
152
|
-
- lib/octool/generated/config.rb
|
153
|
-
- lib/octool/generated/standard.rb
|
154
150
|
- lib/octool/parser.rb
|
155
151
|
- lib/octool/ssp.rb
|
156
152
|
- lib/octool/system.rb
|
@@ -160,6 +156,10 @@ files:
|
|
160
156
|
- schemas/v1.0.0/component.yaml
|
161
157
|
- schemas/v1.0.0/config.yaml
|
162
158
|
- schemas/v1.0.0/standard.yaml
|
159
|
+
- schemas/v1.0.1/certification.yaml
|
160
|
+
- schemas/v1.0.1/component.yaml
|
161
|
+
- schemas/v1.0.1/config.yaml
|
162
|
+
- schemas/v1.0.1/standard.yaml
|
163
163
|
- templates/ssp.erb
|
164
164
|
homepage: https://github.com/jumanjiman/octool
|
165
165
|
licenses:
|
@@ -1,35 +0,0 @@
|
|
1
|
-
require 'kwalify/util/hashlike'
|
2
|
-
|
3
|
-
module OCTool
|
4
|
-
|
5
|
-
|
6
|
-
class Certification
|
7
|
-
include Kwalify::Util::HashLike
|
8
|
-
def initialize(hash=nil)
|
9
|
-
if hash.nil?
|
10
|
-
return
|
11
|
-
end
|
12
|
-
@certification_key = hash['certification_key']
|
13
|
-
@name = hash['name']
|
14
|
-
@requires = (v=hash['requires']) ? v.map!{|e| e.is_a?(ControlID) ? e : ControlID.new(e)} : v
|
15
|
-
end
|
16
|
-
attr_accessor :certification_key # str
|
17
|
-
attr_accessor :name # str
|
18
|
-
attr_accessor :requires # seq
|
19
|
-
end
|
20
|
-
|
21
|
-
|
22
|
-
class ControlID
|
23
|
-
include Kwalify::Util::HashLike
|
24
|
-
def initialize(hash=nil)
|
25
|
-
if hash.nil?
|
26
|
-
return
|
27
|
-
end
|
28
|
-
@standard_key = hash['standard_key']
|
29
|
-
@control_key = hash['control_key']
|
30
|
-
end
|
31
|
-
attr_accessor :standard_key # str
|
32
|
-
attr_accessor :control_key # str
|
33
|
-
end
|
34
|
-
|
35
|
-
end
|
@@ -1,57 +0,0 @@
|
|
1
|
-
require 'kwalify/util/hashlike'
|
2
|
-
|
3
|
-
module OCTool
|
4
|
-
|
5
|
-
|
6
|
-
class Component
|
7
|
-
include Kwalify::Util::HashLike
|
8
|
-
def initialize(hash=nil)
|
9
|
-
if hash.nil?
|
10
|
-
return
|
11
|
-
end
|
12
|
-
@name = hash['name']
|
13
|
-
@component_key = hash['component_key']
|
14
|
-
@description = hash['description']
|
15
|
-
@attestations = (v=hash['attestations']) ? v.map!{|e| e.is_a?(Attestation) ? e : Attestation.new(e)} : v
|
16
|
-
end
|
17
|
-
attr_accessor :name # str
|
18
|
-
attr_accessor :component_key # str
|
19
|
-
attr_accessor :description # str
|
20
|
-
attr_accessor :attestations # seq
|
21
|
-
end
|
22
|
-
|
23
|
-
|
24
|
-
class Attestation
|
25
|
-
include Kwalify::Util::HashLike
|
26
|
-
def initialize(hash=nil)
|
27
|
-
if hash.nil?
|
28
|
-
return
|
29
|
-
end
|
30
|
-
@summary = hash['summary']
|
31
|
-
@status = hash['status']
|
32
|
-
@date_verified = hash['date_verified']
|
33
|
-
@satisfies = (v=hash['satisfies']) ? v.map!{|e| e.is_a?(ControlID) ? e : ControlID.new(e)} : v
|
34
|
-
@narrative = hash['narrative']
|
35
|
-
end
|
36
|
-
attr_accessor :summary # str
|
37
|
-
attr_accessor :status # str
|
38
|
-
attr_accessor :date_verified # date
|
39
|
-
attr_accessor :satisfies # seq
|
40
|
-
attr_accessor :narrative # str
|
41
|
-
end
|
42
|
-
|
43
|
-
|
44
|
-
class ControlID
|
45
|
-
include Kwalify::Util::HashLike
|
46
|
-
def initialize(hash=nil)
|
47
|
-
if hash.nil?
|
48
|
-
return
|
49
|
-
end
|
50
|
-
@standard_key = hash['standard_key']
|
51
|
-
@control_key = hash['control_key']
|
52
|
-
end
|
53
|
-
attr_accessor :standard_key # text
|
54
|
-
attr_accessor :control_key # text
|
55
|
-
end
|
56
|
-
|
57
|
-
end
|
@@ -1,55 +0,0 @@
|
|
1
|
-
require 'kwalify/util/hashlike'
|
2
|
-
|
3
|
-
module OCTool
|
4
|
-
|
5
|
-
|
6
|
-
class Config
|
7
|
-
include Kwalify::Util::HashLike
|
8
|
-
def initialize(hash=nil)
|
9
|
-
if hash.nil?
|
10
|
-
return
|
11
|
-
end
|
12
|
-
@schema_version = hash['schema_version']
|
13
|
-
@name = hash['name']
|
14
|
-
@overview = hash['overview']
|
15
|
-
@maintainers = hash['maintainers']
|
16
|
-
@metadata = (v=hash['metadata']) && v.is_a?(Hash) ? Metadata.new(v) : v
|
17
|
-
@includes = (v=hash['includes']) ? v.map!{|e| e.is_a?(Include) ? e : Include.new(e)} : v
|
18
|
-
end
|
19
|
-
attr_accessor :schema_version # str
|
20
|
-
attr_accessor :name # str
|
21
|
-
attr_accessor :overview # str
|
22
|
-
attr_accessor :maintainers # seq
|
23
|
-
attr_accessor :metadata # map
|
24
|
-
attr_accessor :includes # seq
|
25
|
-
end
|
26
|
-
|
27
|
-
## Optional metadata.
|
28
|
-
class Metadata
|
29
|
-
include Kwalify::Util::HashLike
|
30
|
-
def initialize(hash=nil)
|
31
|
-
if hash.nil?
|
32
|
-
return
|
33
|
-
end
|
34
|
-
@abstract = hash['abstract']
|
35
|
-
@description = hash['description']
|
36
|
-
end
|
37
|
-
attr_accessor :abstract # str
|
38
|
-
attr_accessor :description # str
|
39
|
-
end
|
40
|
-
|
41
|
-
|
42
|
-
class Include
|
43
|
-
include Kwalify::Util::HashLike
|
44
|
-
def initialize(hash=nil)
|
45
|
-
if hash.nil?
|
46
|
-
return
|
47
|
-
end
|
48
|
-
@type = hash['type']
|
49
|
-
@path = hash['path']
|
50
|
-
end
|
51
|
-
attr_accessor :type # str
|
52
|
-
attr_accessor :path # str
|
53
|
-
end
|
54
|
-
|
55
|
-
end
|
@@ -1,55 +0,0 @@
|
|
1
|
-
require 'kwalify/util/hashlike'
|
2
|
-
|
3
|
-
module OCTool
|
4
|
-
|
5
|
-
|
6
|
-
class Standard
|
7
|
-
include Kwalify::Util::HashLike
|
8
|
-
def initialize(hash=nil)
|
9
|
-
if hash.nil?
|
10
|
-
return
|
11
|
-
end
|
12
|
-
@name = hash['name']
|
13
|
-
@standard_key = hash['standard_key']
|
14
|
-
@families = (v=hash['families']) ? v.map!{|e| e.is_a?(ControlFamily) ? e : ControlFamily.new(e)} : v
|
15
|
-
@controls = (v=hash['controls']) ? v.map!{|e| e.is_a?(Control) ? e : Control.new(e)} : v
|
16
|
-
end
|
17
|
-
attr_accessor :name # str
|
18
|
-
attr_accessor :standard_key # str
|
19
|
-
attr_accessor :families # seq
|
20
|
-
attr_accessor :controls # seq
|
21
|
-
end
|
22
|
-
|
23
|
-
|
24
|
-
class ControlFamily
|
25
|
-
include Kwalify::Util::HashLike
|
26
|
-
def initialize(hash=nil)
|
27
|
-
if hash.nil?
|
28
|
-
return
|
29
|
-
end
|
30
|
-
@family_key = hash['family_key']
|
31
|
-
@name = hash['name']
|
32
|
-
end
|
33
|
-
attr_accessor :family_key # str
|
34
|
-
attr_accessor :name # str
|
35
|
-
end
|
36
|
-
|
37
|
-
|
38
|
-
class Control
|
39
|
-
include Kwalify::Util::HashLike
|
40
|
-
def initialize(hash=nil)
|
41
|
-
if hash.nil?
|
42
|
-
return
|
43
|
-
end
|
44
|
-
@control_key = hash['control_key']
|
45
|
-
@family_key = hash['family_key']
|
46
|
-
@name = hash['name']
|
47
|
-
@description = hash['description']
|
48
|
-
end
|
49
|
-
attr_accessor :control_key # str
|
50
|
-
attr_accessor :family_key # str
|
51
|
-
attr_accessor :name # str
|
52
|
-
attr_accessor :description # str
|
53
|
-
end
|
54
|
-
|
55
|
-
end
|