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