inspec_tools 2.0.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.md +15 -0
  3. data/README.md +373 -0
  4. data/Rakefile +96 -0
  5. data/exe/inspec_tools +14 -0
  6. data/lib/data/README.TXT +25 -0
  7. data/lib/data/U_CCI_List.xml +38403 -0
  8. data/lib/data/attributes.yml +23 -0
  9. data/lib/data/cci2html.xsl +136 -0
  10. data/lib/data/cis_to_nist_critical_controls +0 -0
  11. data/lib/data/cis_to_nist_mapping +0 -0
  12. data/lib/data/mapping.yml +17 -0
  13. data/lib/data/rubocop.yml +4 -0
  14. data/lib/data/stig.csv +1 -0
  15. data/lib/data/threshold.yaml +83 -0
  16. data/lib/exceptions/impact_input_error.rb +6 -0
  17. data/lib/exceptions/severity_input_error.rb +6 -0
  18. data/lib/happy_mapper_tools/benchmark.rb +161 -0
  19. data/lib/happy_mapper_tools/cci_attributes.rb +66 -0
  20. data/lib/happy_mapper_tools/stig_attributes.rb +216 -0
  21. data/lib/happy_mapper_tools/stig_checklist.rb +99 -0
  22. data/lib/inspec_tools.rb +17 -0
  23. data/lib/inspec_tools/ckl.rb +20 -0
  24. data/lib/inspec_tools/cli.rb +31 -0
  25. data/lib/inspec_tools/csv.rb +101 -0
  26. data/lib/inspec_tools/help.rb +9 -0
  27. data/lib/inspec_tools/help/compliance.md +7 -0
  28. data/lib/inspec_tools/help/csv2inspec.md +5 -0
  29. data/lib/inspec_tools/help/inspec2ckl.md +5 -0
  30. data/lib/inspec_tools/help/inspec2csv.md +5 -0
  31. data/lib/inspec_tools/help/inspec2xccdf.md +5 -0
  32. data/lib/inspec_tools/help/pdf2inspec.md +6 -0
  33. data/lib/inspec_tools/help/summary.md +5 -0
  34. data/lib/inspec_tools/help/xccdf2inspec.md +5 -0
  35. data/lib/inspec_tools/inspec.rb +331 -0
  36. data/lib/inspec_tools/pdf.rb +125 -0
  37. data/lib/inspec_tools/plugin.rb +15 -0
  38. data/lib/inspec_tools/plugin_cli.rb +275 -0
  39. data/lib/inspec_tools/summary.rb +126 -0
  40. data/lib/inspec_tools/version.rb +8 -0
  41. data/lib/inspec_tools/xccdf.rb +156 -0
  42. data/lib/inspec_tools/xlsx_tool.rb +135 -0
  43. data/lib/inspec_tools_plugin.rb +7 -0
  44. data/lib/overrides/false_class.rb +5 -0
  45. data/lib/overrides/nil_class.rb +5 -0
  46. data/lib/overrides/object.rb +5 -0
  47. data/lib/overrides/string.rb +5 -0
  48. data/lib/overrides/true_class.rb +5 -0
  49. data/lib/utilities/cis_to_nist.rb +11 -0
  50. data/lib/utilities/csv_util.rb +14 -0
  51. data/lib/utilities/extract_pdf_text.rb +20 -0
  52. data/lib/utilities/inspec_util.rb +441 -0
  53. data/lib/utilities/parser.rb +373 -0
  54. data/lib/utilities/text_cleaner.rb +69 -0
  55. metadata +359 -0
@@ -0,0 +1,125 @@
1
+ require 'digest'
2
+
3
+ require_relative '../utilities/inspec_util'
4
+ require_relative '../utilities/extract_pdf_text'
5
+ require_relative '../utilities/parser'
6
+ require_relative '../utilities/text_cleaner'
7
+ require_relative '../utilities/cis_to_nist'
8
+
9
+ # rubocop:disable Metrics/AbcSize
10
+ # rubocop:disable Metrics/PerceivedComplexity
11
+ # rubocop:disable Metrics/CyclomaticComplexity
12
+
13
+ module InspecTools
14
+ class PDF
15
+ def initialize(pdf, profile_name, debug = false)
16
+ raise ArgumentError if pdf.nil?
17
+
18
+ @pdf = pdf
19
+ @name = profile_name
20
+ @debug = debug
21
+ end
22
+
23
+ def to_inspec
24
+ @controls = []
25
+ @csv_handle = nil
26
+ @cci_xml = nil
27
+ @nist_mapping = Utils::CisToNist.get_mapping('cis_to_nist_critical_controls')
28
+ @pdf_text = ''
29
+ @clean_text = ''
30
+ @transformed_data = ''
31
+ @profile = {}
32
+ read_pdf
33
+ @title ||= extract_title
34
+ clean_pdf_text
35
+ transform_data
36
+ insert_json_metadata
37
+ @profile['controls'] = parse_controls
38
+ @profile['sha256'] = Digest::SHA256.hexdigest @profile.to_s
39
+ @profile
40
+ end
41
+
42
+ def to_csv
43
+ # TODO: to_csv
44
+ end
45
+
46
+ def to_xccdf
47
+ # TODO: to_xccdf
48
+ end
49
+
50
+ def to_ckl
51
+ # TODO: to_ckl
52
+ end
53
+
54
+ private
55
+
56
+ # converts passed in data into InSpec format
57
+ def parse_controls
58
+ controls = []
59
+ @transformed_data.each do |contr|
60
+ nist = find_nist(contr[:cis]) unless contr[:cis] == 'No CIS Control'
61
+ control = {}
62
+ control['id'] = 'M-' + contr[:title].split(' ')[0]
63
+ control['title'] = contr[:title]
64
+ control['desc'] = contr[:descr]
65
+ control['impact'] = Utils::InspecUtil.get_impact('medium')
66
+ control['tags'] = {}
67
+ control['tags']['severity'] = Utils::InspecUtil.get_impact_string(control['impact'])
68
+ control['tags']['ref'] = contr[:ref] unless contr[:ref].nil?
69
+ control['tags']['applicability'] = contr[:applicability] unless contr[:applicability].nil?
70
+ control['tags']['cis_id'] = contr[:title].split(' ')[0] unless contr[:title].nil?
71
+ control['tags']['cis_control'] = [contr[:cis], @nist_mapping[0][:cis_ver]] unless contr[:cis].nil? # tag cis_control: [5, 6.1] ##6.1 is the version
72
+ control['tags']['cis_level'] = contr[:level] unless contr[:level].nil?
73
+ control['tags']['nist'] = nist unless nist.nil? # tag nist: [AC-3, 4] ##4 is the version
74
+ control['tags']['check'] = contr[:check] unless contr[:check].nil?
75
+ control['tags']['fix'] = contr[:fix] unless contr[:fix].nil?
76
+ control['tags']['Default Value'] = contr[:default] unless contr[:default].nil?
77
+ controls << control
78
+ end
79
+ controls
80
+ end
81
+
82
+ def insert_json_metadata
83
+ @profile['name'] = @name
84
+ @profile['title'] = @title
85
+ @profile['maintainer'] = 'The Authors'
86
+ @profile['copyright'] = 'The Authors'
87
+ @profile['copyright_email'] = 'you@example.com'
88
+ @profile['license'] = 'Apache-2.0'
89
+ @profile['summary'] = 'An InSpec Compliance Profile'
90
+ @profile['version'] = '0.1.0'
91
+ @profile['supports'] = []
92
+ @profile['attributes'] = []
93
+ @profile['generator'] = {
94
+ 'name': 'inspec_tools',
95
+ 'version': VERSION
96
+ }
97
+ end
98
+
99
+ def extract_title
100
+ @pdf_text.match(/([^\n]*)\n/).captures[0]
101
+ end
102
+
103
+ def read_pdf
104
+ @pdf_text = Util::ExtractPdfText.new(@pdf).extracted_text
105
+ write_pdf_text if @debug
106
+ end
107
+
108
+ def clean_pdf_text
109
+ @clean_text = Util::TextCleaner.new.clean_data(@pdf_text)
110
+ write_clean_text if @debug
111
+ end
112
+
113
+ def transform_data
114
+ @transformed_data = Util::PrepareData.new(@clean_text).transformed_data
115
+ end
116
+
117
+ def write_pdf_text
118
+ File.write('pdf_text', @pdf_text)
119
+ end
120
+
121
+ def write_clean_text
122
+ File.write('debug_text', @clean_text)
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module InspecToolsPlugin
4
+ class Plugin < Inspec.plugin(2)
5
+ # Metadata
6
+ # Must match entry in plugins.json
7
+ plugin_name :'inspec-tools_plugin'
8
+
9
+ # Activation hooks (CliCommand as an example)
10
+ cli_command :tools do
11
+ require_relative 'plugin_cli'
12
+ InspecPlugins::InspecToolsPlugin::CliCommand
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,275 @@
1
+ require 'yaml'
2
+ require 'json'
3
+ require 'roo'
4
+ require_relative '../utilities/inspec_util'
5
+ require_relative '../utilities/csv_util'
6
+
7
+ module InspecTools
8
+ autoload :Help, 'inspec_tools/help'
9
+ autoload :Command, 'inspec_tools/command'
10
+ autoload :XCCDF, 'inspec_tools/xccdf'
11
+ autoload :PDF, 'inspec_tools/pdf'
12
+ autoload :CSVTool, 'inspec_tools/csv'
13
+ autoload :CKL, 'inspec_tools/ckl'
14
+ autoload :Inspec, 'inspec_tools/inspec'
15
+ autoload :Summary, 'inspec_tools/summary'
16
+ autoload :Threshold, 'inspec_tools/threshold'
17
+ autoload :XLSXTool, 'inspec_tools/xlsx_tool'
18
+ end
19
+
20
+ # rubocop:disable Style/GuardClause
21
+ module InspecPlugins
22
+ module InspecToolsPlugin
23
+ class CliCommand < Inspec.plugin(2, :cli_command) # rubocop:disable Metrics/ClassLength
24
+ POSSIBLE_LOG_LEVELS = %w{debug info warn error fatal}.freeze
25
+
26
+ class_option :log_directory, type: :string, aliases: :l, desc: 'Provie log location'
27
+ class_option :log_level, type: :string, desc: "Set the logging level: #{POSSIBLE_LOG_LEVELS}"
28
+
29
+ subcommand_desc 'tools [COMMAND]', 'Runs inspec_tools commands through Inspec'
30
+
31
+ desc 'xccdf2inspec', 'xccdf2inspec translates an xccdf file to an inspec profile'
32
+ long_desc InspecTools::Help.text(:xccdf2inspec)
33
+ option :xccdf, required: true, aliases: '-x'
34
+ option :attributes, required: false, aliases: '-a'
35
+ option :output, required: false, aliases: '-o', default: 'profile'
36
+ option :format, required: false, aliases: '-f', enum: %w{ruby hash}, default: 'ruby'
37
+ option :separate_files, required: false, type: :boolean, default: true, aliases: '-s'
38
+ option :replace_tags, type: :array, required: false, aliases: '-r'
39
+ option :metadata, required: false, aliases: '-m'
40
+ def xccdf2inspec
41
+ xccdf = InspecTools::XCCDF.new(File.read(options[:xccdf]), options[:replace_tags])
42
+ profile = xccdf.to_inspec
43
+
44
+ if !options[:metadata].nil?
45
+ xccdf.inject_metadata(File.read(options[:metadata]))
46
+ end
47
+
48
+ Utils::InspecUtil.unpack_inspec_json(options[:output], profile, options[:separate_files], options[:format])
49
+ if !options[:attributes].nil?
50
+ attributes = xccdf.to_attributes
51
+ File.write(options[:attributes], YAML.dump(attributes))
52
+ end
53
+ end
54
+
55
+ desc 'inspec2xccdf', 'inspec2xccdf translates an inspec profile and attributes files to an xccdf file'
56
+ long_desc InspecTools::Help.text(:inspec2xccdf)
57
+ option :inspec_json, required: true, aliases: '-j'
58
+ option :attributes, required: true, aliases: '-a'
59
+ option :output, required: true, aliases: '-o'
60
+ def inspec2xccdf
61
+ json = File.read(options[:inspec_json])
62
+ inspec_tool = InspecTools::Inspec.new(json)
63
+ attr_hsh = YAML.load_file(options[:attributes])
64
+ xccdf = inspec_tool.to_xccdf(attr_hsh)
65
+ File.write(options[:output], xccdf)
66
+ end
67
+
68
+ desc 'csv2inspec', 'csv2inspec translates CSV to Inspec controls using a mapping file'
69
+ long_desc InspecTools::Help.text(:csv2inspec)
70
+ option :csv, required: true, aliases: '-c'
71
+ option :mapping, required: true, aliases: '-m'
72
+ option :verbose, required: false, type: :boolean, aliases: '-V'
73
+ option :output, required: false, aliases: '-o', default: 'profile'
74
+ option :format, required: false, aliases: '-f', enum: %w{ruby hash}, default: 'ruby'
75
+ option :separate_files, required: false, type: :boolean, default: true, aliases: '-s'
76
+ def csv2inspec
77
+ csv = CSV.read(options[:csv], encoding: 'ISO8859-1')
78
+ mapping = YAML.load_file(options[:mapping])
79
+ profile = InspecTools::CSVTool.new(csv, mapping, options[:csv].split('/')[-1].split('.')[0], options[:verbose]).to_inspec
80
+ Utils::InspecUtil.unpack_inspec_json(options[:output], profile, options[:separate_files], options[:format])
81
+ end
82
+
83
+ desc 'xlsx2inspec', 'xlsx2inspec translates CIS XLSX to Inspec controls using a mapping file'
84
+ long_desc InspecTools::Help.text(:xlsx2inspec)
85
+ option :xlsx, required: true, aliases: '-x'
86
+ option :mapping, required: true, aliases: '-m'
87
+ option :control_name_prefix, required: true, aliases: '-p'
88
+ option :verbose, required: false, type: :boolean, aliases: '-V'
89
+ option :output, required: false, aliases: '-o', default: 'profile'
90
+ option :format, required: false, aliases: '-f', enum: %w{ruby hash}, default: 'ruby'
91
+ option :separate_files, required: false, type: :boolean, default: true, aliases: '-s'
92
+ def xlsx2inspec
93
+ xlsx = Roo::Spreadsheet.open(options[:xlsx])
94
+ mapping = YAML.load_file(options[:mapping])
95
+ profile = InspecTools::XLSXTool.new(xlsx, mapping, options[:xlsx].split('/')[-1].split('.')[0], options[:verbose]).to_inspec(options[:control_name_prefix])
96
+ Utils::InspecUtil.unpack_inspec_json(options[:output], profile, options[:separate_files], options[:format])
97
+ end
98
+
99
+ desc 'inspec2csv', 'inspec2csv translates Inspec controls to CSV'
100
+ long_desc InspecTools::Help.text(:inspec2csv)
101
+ option :inspec_json, required: true, aliases: '-j'
102
+ option :output, required: true, aliases: '-o'
103
+ option :verbose, required: false, type: :boolean, aliases: '-V'
104
+ def inspec2csv
105
+ csv = InspecTools::Inspec.new(File.read(options[:inspec_json])).to_csv
106
+ Utils::CSVUtil.unpack_csv(csv, options[:output])
107
+ end
108
+
109
+ desc 'inspec2ckl', 'inspec2ckl translates an inspec json file to a Checklist file'
110
+ long_desc InspecTools::Help.text(:inspec2ckl)
111
+ option :inspec_json, required: true, aliases: '-j'
112
+ option :output, required: true, aliases: '-o'
113
+ option :verbose, type: :boolean, aliases: '-V'
114
+ option :metadata, required: false, aliases: '-m'
115
+ def inspec2ckl
116
+ metadata = '{}'
117
+ if !options[:metadata].nil?
118
+ metadata = File.read(options[:metadata])
119
+ end
120
+ ckl = InspecTools::Inspec.new(File.read(options[:inspec_json]), metadata).to_ckl
121
+ File.write(options[:output], ckl)
122
+ end
123
+
124
+ desc 'pdf2inspec', 'pdf2inspec translates a PDF Security Control Speficication to Inspec Security Profile'
125
+ long_desc InspecTools::Help.text(:pdf2inspec)
126
+ option :pdf, required: true, aliases: '-p'
127
+ option :output, required: false, aliases: '-o', default: 'profile'
128
+ option :debug, required: false, aliases: '-d', type: :boolean, default: false
129
+ option :format, required: false, aliases: '-f', enum: %w{ruby hash}, default: 'ruby'
130
+ option :separate_files, required: false, type: :boolean, default: true, aliases: '-s'
131
+ def pdf2inspec
132
+ pdf = File.open(options[:pdf])
133
+ profile = InspecTools::PDF.new(pdf, options[:output], options[:debug]).to_inspec
134
+ Utils::InspecUtil.unpack_inspec_json(options[:output], profile, options[:separate_files], options[:format])
135
+ end
136
+
137
+ desc 'generate_map', 'Generates mapping template from CSV to Inspec Controls'
138
+ def generate_map
139
+ template = '
140
+ # Setting csv_header to true will skip the csv file header
141
+ skip_csv_header: true
142
+ width : 80
143
+
144
+
145
+ control.id: 0
146
+ control.title: 15
147
+ control.desc: 16
148
+ control.tags:
149
+ severity: 1
150
+ rid: 8
151
+ stig_id: 3
152
+ cci: 2
153
+ check: 12
154
+ fix: 10
155
+ '
156
+ myfile = File.new('mapping.yml', 'w')
157
+ myfile.puts template
158
+ myfile.close
159
+ end
160
+
161
+ desc 'generate_ckl_metadata', 'Generate metadata file that can be passed to inspec2ckl'
162
+ def generate_ckl_metadata
163
+ metadata = {}
164
+
165
+ metadata['stigid'] = ask('STID ID: ')
166
+ metadata['role'] = ask('Role: ')
167
+ metadata['type'] = ask('Type: ')
168
+ metadata['hostname'] = ask('Hostname: ')
169
+ metadata['ip'] = ask('IP Address: ')
170
+ metadata['mac'] = ask('MAC Address: ')
171
+ metadata['fqdn'] = ask('FQDN: ')
172
+ metadata['tech_area'] = ask('Tech Area: ')
173
+ metadata['target_key'] = ask('Target Key: ')
174
+ metadata['web_or_database'] = ask('Web or Database: ')
175
+ metadata['web_db_site'] = ask('Web DB Site: ')
176
+ metadata['web_db_instance'] = ask('Web DB Instance: ')
177
+
178
+ metadata.delete_if { |_key, value| value.empty? }
179
+ File.open('metadata.json', 'w') do |f|
180
+ f.write(metadata.to_json)
181
+ end
182
+ end
183
+
184
+ desc 'generate_inspec_metadata', 'Generate mapping file that can be passed to xccdf2inspec'
185
+ def generate_inspec_metadata
186
+ metadata = {}
187
+
188
+ metadata['maintainer'] = ask('Maintainer: ')
189
+ metadata['copyright'] = ask('Copyright: ')
190
+ metadata['copyright_email'] = ask('Copyright Email: ')
191
+ metadata['license'] = ask('License: ')
192
+ metadata['version'] = ask('Version: ')
193
+
194
+ metadata.delete_if { |_key, value| value.empty? }
195
+ File.open('metadata.json', 'w') do |f|
196
+ f.write(metadata.to_json)
197
+ end
198
+ end
199
+
200
+ desc 'summary', 'summary parses an inspec results json to create a summary json'
201
+ long_desc InspecTools::Help.text(:summary)
202
+ option :inspec_json, required: true, aliases: '-j'
203
+ option :verbose, type: :boolean, aliases: '-V'
204
+ option :json_full, type: :boolean, required: false, aliases: '-f'
205
+ option :json_counts, type: :boolean, required: false, aliases: '-k'
206
+
207
+ def summary
208
+ summary = InspecTools::Summary.new(File.read(options[:inspec_json])).to_summary
209
+
210
+ unless options.include?('json_full') || options.include?('json_counts')
211
+ puts "\nOverall compliance: #{summary[:compliance]}%\n\n"
212
+ summary[:status].keys.each do |category|
213
+ puts category
214
+ summary[:status][category].keys.each do |impact|
215
+ puts "\t#{impact} : #{summary[:status][category][impact]}"
216
+ end
217
+ end
218
+ end
219
+
220
+ json_summary = summary.to_json
221
+ puts json_summary if options[:json_full]
222
+ puts summary[:status].to_json if options[:json_counts]
223
+ end
224
+
225
+ desc 'compliance', 'compliance parses an inspec results json to check if the compliance level meets a specified threshold'
226
+ long_desc InspecTools::Help.text(:compliance)
227
+ option :inspec_json, required: true, aliases: '-j'
228
+ option :threshold_file, required: false, aliases: '-f'
229
+ option :threshold_inline, required: false, aliases: '-i'
230
+ option :verbose, type: :boolean, aliases: '-V'
231
+
232
+ def compliance
233
+ if options[:threshold_file].nil? && options[:threshold_inline].nil?
234
+ puts 'Please provide threshold as a yaml file or inline yaml'
235
+ exit(1)
236
+ end
237
+ threshold = YAML.load_file(options[:threshold_file]) unless options[:threshold_file].nil?
238
+ threshold = YAML.safe_load(options[:threshold_inline]) unless options[:threshold_inline].nil?
239
+ compliance = InspecTools::Summary.new(File.read(options[:inspec_json])).threshold(threshold)
240
+ compliance ? exit(0) : exit(1)
241
+ end
242
+ end
243
+ end
244
+ end
245
+
246
+ #=====================================================================#
247
+ # Pre-Flight Code
248
+ #=====================================================================#
249
+ help_commands = ['-h', '--help', 'help']
250
+ log_commands = ['-l', '--log-directory']
251
+ version_commands = ['-v', '--version', 'version']
252
+
253
+ #---------------------------------------------------------------------#
254
+ # Adjustments for non-required version commands
255
+ #---------------------------------------------------------------------#
256
+ unless (version_commands & ARGV).empty?
257
+ puts InspecTools::VERSION
258
+ exit 0
259
+ end
260
+
261
+ #---------------------------------------------------------------------#
262
+ # Adjustments for non-required log-directory
263
+ #---------------------------------------------------------------------#
264
+ ARGV.push("--log-directory=#{Dir.pwd}/logs") if (log_commands & ARGV).empty? && (help_commands & ARGV).empty?
265
+
266
+ # Push help to front of command so thor recognizes subcommands are called with help
267
+ if help_commands.any? { |cmd| ARGV.include? cmd }
268
+ help_commands.each do |cmd|
269
+ if (match = ARGV.delete(cmd))
270
+ ARGV.unshift match
271
+ end
272
+ end
273
+ end
274
+
275
+ # rubocop:enable Style/GuardClause
@@ -0,0 +1,126 @@
1
+ require 'json'
2
+ require 'yaml'
3
+ require_relative '../utilities/inspec_util'
4
+
5
+ # rubocop:disable Metrics/AbcSize
6
+
7
+ # Impact Definitions
8
+ CRITICAL = 0.9
9
+ HIGH = 0.7
10
+ MEDIUM = 0.5
11
+ LOW = 0.3
12
+
13
+ BUCKETS = %i(failed passed no_impact skipped error).freeze
14
+ TALLYS = %i(total critical high medium low).freeze
15
+
16
+ THRESHOLD_TEMPLATE = File.expand_path('../data/threshold.yaml', File.dirname(__FILE__))
17
+
18
+ module InspecTools
19
+ class Summary
20
+ def initialize(inspec_json)
21
+ @json = JSON.parse(inspec_json)
22
+ end
23
+
24
+ def to_summary
25
+ @data = Utils::InspecUtil.parse_data_for_ckl(@json)
26
+ @summary = {}
27
+ @data.keys.each do |control_id|
28
+ current_control = @data[control_id]
29
+ current_control[:compliance_status] = Utils::InspecUtil.control_status(current_control, true)
30
+ current_control[:finding_details] = Utils::InspecUtil.control_finding_details(current_control, current_control[:compliance_status])
31
+ end
32
+ compute_summary
33
+ @summary
34
+ end
35
+
36
+ def threshold(threshold = nil)
37
+ @summary = to_summary
38
+ @threshold = Utils::InspecUtil.to_dotted_hash(YAML.load_file(THRESHOLD_TEMPLATE))
39
+ parse_threshold(Utils::InspecUtil.to_dotted_hash(threshold))
40
+ threshold_compliance
41
+ end
42
+
43
+ private
44
+
45
+ def compute_summary
46
+ @summary[:buckets] = {}
47
+ @summary[:buckets][:failed] = select_by_status(@data, 'Open')
48
+ @summary[:buckets][:passed] = select_by_status(@data, 'NotAFinding')
49
+ @summary[:buckets][:no_impact] = select_by_status(@data, 'Not_Applicable')
50
+ @summary[:buckets][:skipped] = select_by_status(@data, 'Not_Reviewed')
51
+ @summary[:buckets][:error] = select_by_status(@data, 'Profile_Error')
52
+
53
+ @summary[:status] = {}
54
+ @summary[:status][:failed] = tally_by_impact(@summary[:buckets][:failed])
55
+ @summary[:status][:passed] = tally_by_impact(@summary[:buckets][:passed])
56
+ @summary[:status][:no_impact] = tally_by_impact(@summary[:buckets][:no_impact])
57
+ @summary[:status][:skipped] = tally_by_impact(@summary[:buckets][:skipped])
58
+ @summary[:status][:error] = tally_by_impact(@summary[:buckets][:error])
59
+
60
+ @summary[:compliance] = compute_compliance
61
+ end
62
+
63
+ def select_by_impact(controls, impact)
64
+ controls.select { |_key, value| value[:impact].to_f.eql?(impact) }
65
+ end
66
+
67
+ def select_by_status(controls, status)
68
+ controls.select { |_key, value| value[:compliance_status].eql?(status) }
69
+ end
70
+
71
+ def tally_by_impact(controls)
72
+ tally = {}
73
+ tally[:total] = controls.count
74
+ tally[:critical] = select_by_impact(controls, CRITICAL).count
75
+ tally[:high] = select_by_impact(controls, HIGH).count
76
+ tally[:medium] = select_by_impact(controls, MEDIUM).count
77
+ tally[:low] = select_by_impact(controls, LOW).count
78
+ tally
79
+ end
80
+
81
+ def compute_compliance
82
+ (@summary[:status][:passed][:total]*100.0/
83
+ (@summary[:status][:passed][:total]+
84
+ @summary[:status][:failed][:total]+
85
+ @summary[:status][:skipped][:total]+
86
+ @summary[:status][:error][:total])).floor
87
+ end
88
+
89
+ def threshold_compliance
90
+ compliance = true
91
+ failure = []
92
+ max = @threshold['compliance.max']
93
+ min = @threshold['compliance.min']
94
+ if max != -1 and @summary[:compliance] > max
95
+ compliance = false
96
+ failure << "Expected compliance.max:#{max} got:#{@summary[:compliance]}"
97
+ end
98
+ if min != -1 and @summary[:compliance] < min
99
+ compliance = false
100
+ failure << "Expected compliance.min:#{min} got:#{@summary[:compliance]}"
101
+ end
102
+ status = @summary[:status]
103
+ BUCKETS.each do |bucket|
104
+ TALLYS.each do |tally|
105
+ max = @threshold["#{bucket}.#{tally}.max"]
106
+ min = @threshold["#{bucket}.#{tally}.min"]
107
+ if max != -1 and status[bucket][tally] > max
108
+ compliance = false
109
+ failure << "Expected #{bucket}.#{tally}.max:#{max} got:#{status[bucket][tally]}"
110
+ end
111
+ if min != -1 and status[bucket][tally] < min
112
+ compliance = false
113
+ failure << "Expected #{bucket}.#{tally}.min:#{min} got:#{status[bucket][tally]}"
114
+ end
115
+ end
116
+ end
117
+ puts failure.join("\n") unless compliance
118
+ puts 'Compliance threshold met' if compliance
119
+ compliance
120
+ end
121
+
122
+ def parse_threshold(new_threshold)
123
+ @threshold.merge!(new_threshold)
124
+ end
125
+ end
126
+ end