csa-ccm 0.1.0

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