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.
@@ -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
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require_relative "../lib/csa/ccm/cli"
5
+ require_relative "../lib/csa/ccm/cli/command"
6
+
7
+ Csa::Ccm::Cli::Command.start(ARGV)
8
+
9
+ exit 0
@@ -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
@@ -0,0 +1,10 @@
1
+ module Csa::Ccm
2
+ module Cli
3
+
4
+ end
5
+ end
6
+
7
+ require_relative "cli/resource"
8
+ require_relative "cli/ui"
9
+ require_relative "matrix"
10
+ require_relative "question"
@@ -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,7 @@
1
+ module Csa
2
+ module Ccm
3
+ module Cli
4
+ VERSION = "0.1.0"
5
+ end
6
+ end
7
+ 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