-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathgenerate.rb
250 lines (217 loc) · 7.02 KB
/
generate.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
require 'roo'
require 'roo-xls'
require 'erb'
require 'facets'
require 'fileutils'
#spreadsheet = 'CIS_Red_Hat_Enterprise_Linux_7_Benchmark_v2.2.0.xls'
spreadsheet = 'CIS_Microsoft_Windows_Server_2016_RTM_Release_1607_Benchmark_v1.1.0.xls'
xls = Roo::Spreadsheet.open(spreadsheet)
#osname = 'linux'
#osversion = 'rhel7'
osname = 'windows'
osversion = 'server2016'
manifest_dir = './manifests'
puppetmodule = 'cis_' + osname
sheet_regex = %r{^(?<level>Level\s\d)?(\s-\s)?(?<profile>.*)$}
#namespace_regex = %r{\A[a-z][a-z0-9_]*\Z}
# Used to map column to the corresponding header of the sheet
headers = {
1 => :section,
2 => :recommendation,
3 => :title,
4 => :status,
5 => :scoring_status,
6 => :description,
7 => :rationale_statement,
8 => :remediation_procedure,
9 => :audit_procedure,
10 => :impact_statement,
11 => :notes,
12 => :cis_controls,
13 => :cce_id,
14 => :references,
}
# easily return parent versions
# https://stackoverflow.com/a/12192707
#class String
# def split_by_last(char=".")
# pos = self.rindex(char)
# pos != nil ? [self[0...pos], self[pos+1..-1]] : [self]
# end
#end
def section_template()
<<EOT
# @summary A short summary of the purpose of this class
#
# Todo: sanatize section[:description] to add context here
#
# @example
# include <%= class_name %>
#
class <%= class_name %> (
Array $benchmark_blacklist = $<%= module_name %>::<%= osversion %>::benchmark_blacklist,
){
<% section[:benchmarks].each do |benchmark_number| %>
# Benchmark: <%= benchmark_number %>
# Title: <%= benchmarks[benchmark_number][:title] %>
# Scoring Status: <%= benchmarks[benchmark_number][:scoring_status] %>
unless '<%= benchmark_number %>' in $benchmark_blacklist {
warning('Resource(s) to manage benchmark <%= benchmark_number %> have not been implemented yet.')
}
<% end %>
}
EOT
end
def main_template()
<<EOT
# @summary A short summary of the purpose of this class
#
# Todo: add description
#
# @example
# include <%= class_name %>
#
class <%= class_name %> (
Array $benchmark_blacklist = []
){
<% section[:benchmarks].each do |benchmark_number| %>
# Benchmark: <%= benchmark_number %>
# Title: <%= benchmarks[benchmark_number][:title] %>
# Scoring Status: <%= benchmarks[benchmark_number][:scoring_status] %>
unless '<%= benchmark_number %>' in $benchmark_blacklist {
warning('Resource(s) to manage benchmark <%= benchmark_number %> have not been implemented yet.')
}
<% end %>
}
EOT
end
# remove invalid characters from namespace
def clean_namespace(string)
namespace_regex = %r{\A[a-z][a-z0-9_]*\Z}
if matches = string.match(namespace_regex)
string
else
cleaned = string.gsub!(/[^a-z0-9_]/, '')
cleaned
end
end
## https://stackoverflow.com/questions/754407/what-is-the-best-way-to-chop-a-string-into-chunks-of-a-given-length-in-ruby
#def chunk(string, size)
# (0..(string.length-1)/size).map{|i|string[i*size,size]}
#end
class CisClass
include ERB::Util
attr_accessor :template, :module_name, :class_name, :section, :benchmarks, :osversion
def initialize(template, module_name, class_name, section, benchmarks, osversion)
@template = template
@section = section
@benchmarks = benchmarks
@class_name = class_name
@module_name = module_name
@osversion = osversion
end
def render()
ERB.new(@template, nil, '-').result(binding)
end
def save(file)
File.open(file, "w+") do |f|
f.write(render)
end
end
end
xls.sheets.each do |sheet_name|
next if sheet_name == 'License'
puts ''
puts '--------------'
puts sheet_name
puts '--------------'
sheet = xls.sheet(sheet_name)
# contains the parsed information from the excel doc
# data[row] == { column_header => cell data, ... }
data = {}
# iterate over each row and parse each column
if !sheet.nil?
# manually limiting for testing
# last_row = 10
last_row = sheet.last_row
last_column = sheet.last_column
if !last_row.nil? and !last_column.nil?
for row in 2..last_row # skip first row (headers)
data[row] = {}
for col in 1..last_column
v = sheet.cell(row, col)
# only keep columns that have data
data[row].merge!(headers[col] => v.to_s) unless v.nil?
end
end
else
puts 'Seems no data in sheet: ' + sheet_name
end
end
sections = {}
benchmarks = {}
# map benchmark to document sections
data.each do |doc_row, column|
# shorthand since this is used a lot
section_number = column[:section]
if column.key?(:recommendation) # if there is a recommendation number then the row describes a benchmark
# shorthand since this is used a lot
benchmark_number = column[:recommendation]
# benchmark numbers should be unique so simply add to benchmarks hash
benchmarks[benchmark_number] = column
top_section = section_number.split('.').first
# logic to add/append to :benchmarks array of the top parent section
if sections[top_section].has_key?(:benchmarks)
sections[top_section][:benchmarks] << benchmark_number
else
sections[top_section][:benchmarks] = [benchmark_number]
end
else # if there is no recommendation number the row describes a section
unless sections.has_key?(section_number)
# section numbers are unique so we can pre-populate with the title and description
sections[section_number] = {:title => column[:title], :description => column[:description]}
end
end
end
level = nil
profile = nil
classname = nil
class_struct = {}
sections.each do |section, section_info|
unless section_info[:benchmarks].nil?
# debugging output
puts
puts section + ' - ' + section_info[:title]
puts "benchmarks: #{section_info[:benchmarks]}"
puts "number benchmarks: #{section_info[:benchmarks].count}"
if matches = sheet_name.match(sheet_regex)
level = matches[:level]
profile = matches[:profile]
else
puts "Failed to parse sheet name"
end
# Figures out the correct classname
if level.nil?
classname = puppetmodule + '::' + osversion + '::' + profile.snakecase + '::' + clean_namespace(section_info[:title].snakecase)
else
classname = puppetmodule + '::' + osversion + '::' + level.snakecase + '::' + profile.snakecase + '::' + clean_namespace(section_info[:title].snakecase)
end
# Initilize ERB class template
manifest = CisClass.new(section_template, puppetmodule, classname, section_info, benchmarks, osversion)
# create directory structure
namespaces = classname.split("::")
manifest_file = manifest_dir + '/' + namespaces[1..(namespaces.size - 1)].join('/') + '.pp'
dirname = File.dirname(manifest_file)
unless File.directory?(dirname)
FileUtils.mkdir_p(dirname)
end
# Write manifest to stdout for debugging
# puts
# puts 'Puppet code:'
# puts manifest.render
# writes out the manifest file
manifest.save(manifest_file)
puts "Manifest written to #{manifest_file}"
end
end
end