csa-ccm 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 +7 -0
- data/3.0.1.yaml +1517 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +57 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/build.rb +46 -0
- data/caiq-3.0.1.yaml +2029 -0
- data/csa-ccm.gemspec +28 -0
- data/exe/csa-ccm +9 -0
- data/lib/csa/ccm/answer.rb +57 -0
- data/lib/csa/ccm/cli.rb +10 -0
- data/lib/csa/ccm/cli/command.rb +113 -0
- data/lib/csa/ccm/cli/resource.rb +34 -0
- data/lib/csa/ccm/cli/ui.rb +18 -0
- data/lib/csa/ccm/cli/version.rb +7 -0
- data/lib/csa/ccm/control.rb +47 -0
- data/lib/csa/ccm/control_domain.rb +49 -0
- data/lib/csa/ccm/matrix.rb +187 -0
- data/lib/csa/ccm/question.rb +32 -0
- data/lib/ext/string.rb +7 -0
- data/resources/csa-caiq-v1.0-10-12-2010.xlsx +0 -0
- data/resources/csa-caiq-v1.1-09-01-2011.xlsx +0 -0
- data/resources/csa-caiq-v3.0.1-09-01-2017.xlsx +0 -0
- data/resources/csa-caiq-v3.0.1-12-05-2016.xlsx +0 -0
- data/resources/~$csa-caiq-v3.0.1-09-01-2017.xlsx +0 -0
- data/samples/ccm-answers.yaml +16 -0
- data/samples/ccm.yaml +34 -0
- data/tmp/ccm-301.yaml +2029 -0
- metadata +144 -0
data/csa-ccm.gemspec
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "csa/ccm/cli/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "csa-ccm"
|
8
|
+
spec.version = Csa::Ccm::Cli::VERSION
|
9
|
+
spec.authors = ["Ribose Inc."]
|
10
|
+
spec.email = ["open.source@ribose.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{Parsing and writing of the CSA CCM}
|
13
|
+
spec.description = %q{Parsing and writing of the CSA CCM located at https://cloudsecurityalliance.org/working-groups/cloud-controls-matrix.}
|
14
|
+
spec.homepage = "https://open.ribose.com"
|
15
|
+
|
16
|
+
spec.files = Dir['**/*'].reject { |f| f.match(%r{^(test|spec|features|.git)/|.(gem|gif|png|jpg|jpeg|xml|html|doc|pdf|dtd|ent)$}) }
|
17
|
+
|
18
|
+
spec.bindir = "exe"
|
19
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
20
|
+
spec.require_paths = ["lib"]
|
21
|
+
|
22
|
+
spec.add_runtime_dependency "rake", "~> 10.0"
|
23
|
+
spec.add_runtime_dependency "rubyXL", "~> 3.4.3"
|
24
|
+
spec.add_runtime_dependency "thor", "~> 0.20.3"
|
25
|
+
|
26
|
+
spec.add_development_dependency "bundler", "~> 2.0"
|
27
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
28
|
+
end
|
data/exe/csa-ccm
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
module Csa::Ccm
|
2
|
+
|
3
|
+
class Answer
|
4
|
+
|
5
|
+
ATTRIBS = %i(
|
6
|
+
id content
|
7
|
+
control-id question-id
|
8
|
+
notes
|
9
|
+
)
|
10
|
+
|
11
|
+
attr_accessor *ATTRIBS
|
12
|
+
|
13
|
+
def initialize(options={})
|
14
|
+
@examples = []
|
15
|
+
@notes = []
|
16
|
+
|
17
|
+
# puts "options #{options.inspect}"
|
18
|
+
|
19
|
+
options.each_pair do |k, v|
|
20
|
+
next unless v
|
21
|
+
case k
|
22
|
+
when /^example/
|
23
|
+
@examples << v
|
24
|
+
when /^note/
|
25
|
+
@notes << v
|
26
|
+
else
|
27
|
+
# puts"Key #{k}"
|
28
|
+
key = k.gsub("-", "_")
|
29
|
+
self.send("#{key}=", v)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
self
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_hash
|
36
|
+
ATTRIBS.inject({}) do |acc, attrib|
|
37
|
+
value = self.send(attrib)
|
38
|
+
unless value.nil?
|
39
|
+
acc.merge(attrib.to_s => value)
|
40
|
+
else
|
41
|
+
acc
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# # entry-status
|
47
|
+
# ## Must be one of notValid valid superseded retired
|
48
|
+
# def entry_status=(value)
|
49
|
+
# unless %w(notValid valid superseded retired).include?(value)
|
50
|
+
# value = "notValid"
|
51
|
+
# end
|
52
|
+
# @entry_status = value
|
53
|
+
# end
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
data/lib/csa/ccm/cli.rb
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'thor'
|
3
|
+
|
4
|
+
require_relative 'ui'
|
5
|
+
require_relative '../cli'
|
6
|
+
|
7
|
+
module Csa
|
8
|
+
module Ccm
|
9
|
+
module Cli
|
10
|
+
class Command < Thor
|
11
|
+
desc 'ccm-yaml VERSION', 'Generating a machine-readable CCM/CAIQ'
|
12
|
+
option :output_file, aliases: :o, type: :string, desc: 'Optional output YAML file. If missed, the input file’s name will be used'
|
13
|
+
|
14
|
+
def ccm_yaml(version)
|
15
|
+
input_files = Resource.lookup_version(version)
|
16
|
+
|
17
|
+
unless input_files || !input_files.empty?
|
18
|
+
UI.say("No file found for #{version} version")
|
19
|
+
return
|
20
|
+
end
|
21
|
+
|
22
|
+
input_file = input_files.first
|
23
|
+
|
24
|
+
unless File.exist? input_file
|
25
|
+
UI.say('No file found for version')
|
26
|
+
return
|
27
|
+
end
|
28
|
+
|
29
|
+
matrix = Matrix.from_xlsx(version, input_file)
|
30
|
+
|
31
|
+
output_file = options[:output_file] || "caiq-#{version}.yaml"
|
32
|
+
matrix.to_file(output_file)
|
33
|
+
end
|
34
|
+
|
35
|
+
desc "xlsx2yaml XSLT_PATH", "Converting CCM XSLX to YAML"
|
36
|
+
option :output_file, aliases: :o, type: :string, desc: "Optional output YAML file. If missed, the input file’s name will be used"
|
37
|
+
|
38
|
+
def xlsx2yaml(xslt_path)
|
39
|
+
raise 'Not implemented yet'
|
40
|
+
end
|
41
|
+
|
42
|
+
desc "caiq2yaml XSLT_PATH", "Converting a filled CAIQ to YAML"
|
43
|
+
option :output_name, aliases: :n, type: :string, desc: "Optional output CAIQ YAML file. If missed, the input file’s name will be used"
|
44
|
+
option :output_path, aliases: :p, type: :string, desc: "Optional output directory for result file. If missed pwd will be used"
|
45
|
+
|
46
|
+
def caiq2yaml(xslt_path)
|
47
|
+
# if xslt_path.nil?
|
48
|
+
# puts 'Error: no filepath given as first argument.'
|
49
|
+
# exit 1
|
50
|
+
# end
|
51
|
+
|
52
|
+
# if Pathname.new(xslt_path).extname != ".xlsx"
|
53
|
+
# puts 'Error: filepath given must have extension .xlsx.'
|
54
|
+
# exit 1
|
55
|
+
# end
|
56
|
+
|
57
|
+
# workbook = Csa::Ccm::MatrixWorkbook.new(xslt_path)
|
58
|
+
# workbook.glossary_info.metadata_section.structure
|
59
|
+
# workbook.glossary_info.metadata_section.attributes
|
60
|
+
|
61
|
+
# languages = {}
|
62
|
+
|
63
|
+
# workbook.languages_supported.map do |lang|
|
64
|
+
# puts "************** WORKING ON LANGUAGE (#{lang})"
|
65
|
+
# sheet = workbook.language_sheet(lang)
|
66
|
+
# termsec = sheet.terms_section
|
67
|
+
# languages[sheet.language_code] = termsec.terms
|
68
|
+
# end
|
69
|
+
|
70
|
+
# collection = Csa::Ccm::ControlDomain.new
|
71
|
+
|
72
|
+
# languages.each_pair do |lang, terms|
|
73
|
+
# terms.each do |term|
|
74
|
+
# collection.add_term(term)
|
75
|
+
# end
|
76
|
+
# end
|
77
|
+
|
78
|
+
# # collection[1206].inspect
|
79
|
+
|
80
|
+
# # FIXME use instea output dir
|
81
|
+
# output_dir = options[:output_path] || Dir.pwd
|
82
|
+
# output_file = options[:output_name] || File.join(output_dir, Pathname.new(filepath).basename.sub_ext(".yaml"))
|
83
|
+
|
84
|
+
# collection.to_file(output_file)
|
85
|
+
|
86
|
+
# collection_output_dir = File.join(output_dir, "concepts")
|
87
|
+
|
88
|
+
# FileUtils.mkdir_p(collection_output_dir)
|
89
|
+
|
90
|
+
# collection.keys.each do |id|
|
91
|
+
# collection[id].to_file(File.join(collection_output_dir, "concept-#{id}.yaml"))
|
92
|
+
# end
|
93
|
+
|
94
|
+
# french = workbook.language_sheet("French")
|
95
|
+
# french.sections[3].structure
|
96
|
+
# french.sections[3].terms
|
97
|
+
|
98
|
+
# english = workbook.language_sheet("English")
|
99
|
+
# english.terms_section
|
100
|
+
# english.terms_section.terms
|
101
|
+
|
102
|
+
#pry.binding
|
103
|
+
end
|
104
|
+
|
105
|
+
desc "generate-with-answers ANSWERS_YAML", "Writing to the CAIQ XSLX template using YAML"
|
106
|
+
|
107
|
+
def generate_with_answers(answers)
|
108
|
+
raise 'Not implemented yet'
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'rubyXL'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
require_relative '../../../ext/string'
|
5
|
+
|
6
|
+
module Csa
|
7
|
+
module Ccm
|
8
|
+
module Cli
|
9
|
+
class Resource
|
10
|
+
def self.root_gem
|
11
|
+
Pathname.new(__dir__).join('..', '..', '..', '..')
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.lookup_version(version)
|
15
|
+
Dir["#{root_gem}/resources/**/*v#{version}*.xlsx"]
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.from_xlsx(xslt_path)
|
19
|
+
raise 'Not implemented yet'
|
20
|
+
end
|
21
|
+
|
22
|
+
def from_caiq(xslt_path, output_name, output_path)
|
23
|
+
raise 'Not implemented yet'
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.to_file(hash, output_file)
|
27
|
+
File.open(output_file, 'w') { |f| f.write hash.to_yaml(line_width: 9999) }
|
28
|
+
rescue Errno::ENOENT => e
|
29
|
+
UI.say("Cannot write result to #{output_file} because: #{e.message}")
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'thor'
|
2
|
+
|
3
|
+
module Csa::Ccm::Cli
|
4
|
+
class UI < Thor
|
5
|
+
def self.ask(message)
|
6
|
+
new.ask(message)
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.say(message)
|
10
|
+
new.say(message)
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.run(command)
|
14
|
+
require "open3"
|
15
|
+
Open3.capture3(command)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Csa::Ccm
|
2
|
+
|
3
|
+
class Control
|
4
|
+
ATTRIBS = %i(
|
5
|
+
id name specification questions
|
6
|
+
)
|
7
|
+
|
8
|
+
attr_accessor *ATTRIBS
|
9
|
+
|
10
|
+
def initialize(options={})
|
11
|
+
options.each_pair do |k,v|
|
12
|
+
self.send("#{k}=", v)
|
13
|
+
end
|
14
|
+
|
15
|
+
@questions ||= {}
|
16
|
+
|
17
|
+
self
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_hash
|
21
|
+
ATTRIBS.inject({}) do |acc, attrib|
|
22
|
+
value = self.send(attrib)
|
23
|
+
|
24
|
+
unless value.nil?
|
25
|
+
if attrib == :questions
|
26
|
+
value = value.inject([]) do |acc, (k, v)|
|
27
|
+
acc << v.to_hash
|
28
|
+
acc
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
acc.merge(attrib.to_s => value)
|
33
|
+
else
|
34
|
+
acc
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def to_file(filename)
|
40
|
+
File.open(filename,"w") do |file|
|
41
|
+
file.write(to_hash.to_yaml)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require_relative "control"
|
2
|
+
|
3
|
+
module Csa::Ccm
|
4
|
+
|
5
|
+
class ControlDomain
|
6
|
+
ATTRIBS = %i(
|
7
|
+
id name controls
|
8
|
+
)
|
9
|
+
|
10
|
+
attr_accessor *ATTRIBS
|
11
|
+
|
12
|
+
def initialize(options={})
|
13
|
+
options.each_pair do |k,v|
|
14
|
+
self.send("#{k}=", v)
|
15
|
+
end
|
16
|
+
|
17
|
+
@controls ||= {}
|
18
|
+
|
19
|
+
self
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_hash
|
23
|
+
ATTRIBS.inject({}) do |acc, attrib|
|
24
|
+
value = self.send(attrib)
|
25
|
+
|
26
|
+
unless value.nil?
|
27
|
+
if attrib == :controls
|
28
|
+
value = value.inject([]) do |acc, (k, v)|
|
29
|
+
acc << v.to_hash
|
30
|
+
acc
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
acc.merge(attrib.to_s => value)
|
35
|
+
else
|
36
|
+
acc
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def to_file(filename)
|
42
|
+
File.open(filename,"w") do |file|
|
43
|
+
file.write(to_hash.to_yaml)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
@@ -0,0 +1,187 @@
|
|
1
|
+
require_relative "control_domain"
|
2
|
+
require_relative "control"
|
3
|
+
require_relative "question"
|
4
|
+
|
5
|
+
module Csa::Ccm
|
6
|
+
|
7
|
+
class Matrix
|
8
|
+
ATTRIBS = %i(
|
9
|
+
version title source_file workbook source_path control_domains
|
10
|
+
)
|
11
|
+
|
12
|
+
attr_accessor *ATTRIBS
|
13
|
+
|
14
|
+
def initialize(options={})
|
15
|
+
options.each_pair do |k,v|
|
16
|
+
self.send("#{k}=", v)
|
17
|
+
end
|
18
|
+
|
19
|
+
if source_path
|
20
|
+
@workbook = RubyXL::Parser.parse(source_path)
|
21
|
+
end
|
22
|
+
|
23
|
+
@control_domains ||= {}
|
24
|
+
|
25
|
+
self
|
26
|
+
end
|
27
|
+
|
28
|
+
def source_file
|
29
|
+
@source_file || File.basename(source_path)
|
30
|
+
end
|
31
|
+
|
32
|
+
def worksheet
|
33
|
+
workbook.worksheets.first
|
34
|
+
end
|
35
|
+
|
36
|
+
def title
|
37
|
+
worksheet = workbook.worksheets.first
|
38
|
+
worksheet[0][2].value
|
39
|
+
end
|
40
|
+
|
41
|
+
def metadata
|
42
|
+
{
|
43
|
+
'version' => version,
|
44
|
+
'title' => title,
|
45
|
+
'source_file' => source_file
|
46
|
+
}
|
47
|
+
end
|
48
|
+
|
49
|
+
class Row
|
50
|
+
ATTRIBS = %i(
|
51
|
+
control_domain_id control_id question_id control_spec
|
52
|
+
question_content answer_yes answer_no answer_na notes
|
53
|
+
control_domain_description
|
54
|
+
)
|
55
|
+
|
56
|
+
attr_accessor *ATTRIBS
|
57
|
+
|
58
|
+
def initialize(ruby_xl_row)
|
59
|
+
@control_domain_description = ruby_xl_row[0].value
|
60
|
+
@control_id = ruby_xl_row[1].value
|
61
|
+
@question_id = ruby_xl_row[2].value
|
62
|
+
@control_spec = ruby_xl_row[3].value
|
63
|
+
@question_content = ruby_xl_row[4].value
|
64
|
+
@answer_yes = ruby_xl_row[5].value
|
65
|
+
@answer_no = ruby_xl_row[6].value
|
66
|
+
@answer_na = ruby_xl_row[7].value
|
67
|
+
|
68
|
+
# In 3.0.1 2017-09-01, Rows 276 and 277's control ID says "LG-02" but it should be "STA-05" instead.
|
69
|
+
@control_id = question_id.split(".").first if question_id
|
70
|
+
@control_domain_id = control_id.split("-").first if control_id
|
71
|
+
|
72
|
+
# puts "HERE IN ROW! #{ruby_xl_row.cells.map(&:value)}"
|
73
|
+
|
74
|
+
puts control_domain_description
|
75
|
+
puts control_id
|
76
|
+
puts question_id
|
77
|
+
|
78
|
+
self
|
79
|
+
end
|
80
|
+
|
81
|
+
def control_domain_name
|
82
|
+
return nil if control_domain_description.nil?
|
83
|
+
name, _, control_name = control_domain_description.split(/(\n)/)
|
84
|
+
name
|
85
|
+
end
|
86
|
+
|
87
|
+
def control_name
|
88
|
+
return nil if control_domain_description.nil?
|
89
|
+
name, _, control_name = control_domain_description.split(/(\n)/)
|
90
|
+
control_name
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def row(i)
|
95
|
+
Row.new(worksheet[i])
|
96
|
+
end
|
97
|
+
|
98
|
+
def self.from_xlsx(version, input_file)
|
99
|
+
matrix = Matrix.new(
|
100
|
+
version: version,
|
101
|
+
source_path: input_file
|
102
|
+
)
|
103
|
+
|
104
|
+
all_rows = matrix.worksheet.sheet_data.rows
|
105
|
+
start_row = 4 # FIXME add some basic logic to calculate it
|
106
|
+
|
107
|
+
last_control_domain = nil
|
108
|
+
last_control_id = nil
|
109
|
+
last_control_specification = nil
|
110
|
+
|
111
|
+
worksheet = matrix.worksheet
|
112
|
+
|
113
|
+
row_number = start_row
|
114
|
+
max_row_number = all_rows.length - 1
|
115
|
+
|
116
|
+
# We loop over all Questions
|
117
|
+
(start_row..max_row_number).each do |row_number|
|
118
|
+
|
119
|
+
puts "looping row #{row_number}"
|
120
|
+
row = matrix.row(row_number)
|
121
|
+
# Skip row if there is no question-id
|
122
|
+
puts "row #{row.question_id}"
|
123
|
+
# require 'pry'
|
124
|
+
# binding.pry
|
125
|
+
next if row.question_id.nil?
|
126
|
+
|
127
|
+
puts "domain_id #{row.control_domain_id}"
|
128
|
+
|
129
|
+
domain_id = row.control_domain_id
|
130
|
+
unless domain_id.nil?
|
131
|
+
|
132
|
+
control_domain = matrix.control_domains[domain_id] ||
|
133
|
+
ControlDomain.new(
|
134
|
+
id: row.control_domain_id,
|
135
|
+
name: row.control_domain_name
|
136
|
+
)
|
137
|
+
|
138
|
+
puts "control_domain #{control_domain.to_hash}"
|
139
|
+
|
140
|
+
# Store the Control Domain
|
141
|
+
matrix.control_domains[domain_id] = control_domain
|
142
|
+
end
|
143
|
+
|
144
|
+
control_id = row.control_id
|
145
|
+
unless control_id.nil?
|
146
|
+
|
147
|
+
control_domain = matrix.control_domains[domain_id]
|
148
|
+
control = control_domain.controls[control_id] || Control.new(
|
149
|
+
id: row.control_id,
|
150
|
+
name: row.control_name,
|
151
|
+
specification: row.control_spec
|
152
|
+
)
|
153
|
+
|
154
|
+
puts "control #{control.to_hash}"
|
155
|
+
# Store the Control
|
156
|
+
control_domain.controls[control_id] = control
|
157
|
+
end
|
158
|
+
|
159
|
+
question = matrix.control_domains[domain_id].controls[control_id]
|
160
|
+
# Store the Question
|
161
|
+
puts question.to_hash
|
162
|
+
control.questions[row.question_id] = Question.new(id: row.question_id, content: row.question_content)
|
163
|
+
end
|
164
|
+
|
165
|
+
matrix
|
166
|
+
end
|
167
|
+
|
168
|
+
def to_hash
|
169
|
+
{
|
170
|
+
"ccm" => {
|
171
|
+
"metadata" => metadata.to_hash,
|
172
|
+
"control_domains" => control_domains.inject([]) do |acc, (k, v)|
|
173
|
+
acc << v.to_hash
|
174
|
+
acc
|
175
|
+
end
|
176
|
+
}
|
177
|
+
}
|
178
|
+
end
|
179
|
+
|
180
|
+
def to_file(filename)
|
181
|
+
File.open(filename,"w") do |file|
|
182
|
+
file.write(to_hash.to_yaml)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
end
|
187
|
+
end
|