inspec_tools 0.0.0.1.ENOTAG

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.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +662 -0
  3. data/LICENSE.md +15 -0
  4. data/README.md +329 -0
  5. data/Rakefile +30 -0
  6. data/exe/inspec_tools +14 -0
  7. data/lib/data/NIST_Map_02052020_CIS_Controls_Version_7.1_Implementation_Groups_1.2.xlsx +0 -0
  8. data/lib/data/NIST_Map_09212017B_CSC-CIS_Critical_Security_Controls_VER_6.1_Excel_9.1.2016.xlsx +0 -0
  9. data/lib/data/README.TXT +25 -0
  10. data/lib/data/U_CCI_List.xml +38403 -0
  11. data/lib/data/attributes.yml +23 -0
  12. data/lib/data/cci2html.xsl +136 -0
  13. data/lib/data/mapping.yml +17 -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 +196 -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 +136 -0
  37. data/lib/inspec_tools/plugin.rb +15 -0
  38. data/lib/inspec_tools/plugin_cli.rb +278 -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 +155 -0
  42. data/lib/inspec_tools/xlsx_tool.rb +148 -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/csv_util.rb +14 -0
  50. data/lib/utilities/extract_nist_cis_mapping.rb +57 -0
  51. data/lib/utilities/extract_pdf_text.rb +20 -0
  52. data/lib/utilities/inspec_util.rb +435 -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,148 @@
1
+ require 'nokogiri'
2
+ require 'inspec-objects'
3
+ require 'word_wrap'
4
+ require 'yaml'
5
+ require 'digest'
6
+ require 'roo'
7
+
8
+ require_relative '../utilities/inspec_util'
9
+
10
+ # rubocop:disable Metrics/AbcSize
11
+ # rubocop:disable Metrics/PerceivedComplexity
12
+ # rubocop:disable Metrics/CyclomaticComplexity
13
+
14
+ module InspecTools
15
+ # Methods for converting from XLS to various formats
16
+ class XLSXTool
17
+ CIS_2_NIST_XLSX = Roo::Spreadsheet.open(File.join(File.dirname(__FILE__), '../data/NIST_Map_02052020_CIS_Controls_Version_7.1_Implementation_Groups_1.2.xlsx'))
18
+ LATEST_NIST_REV = 'Rev_4'.freeze
19
+
20
+ def initialize(xlsx, mapping, name, verbose = false)
21
+ @name = name
22
+ @xlsx = xlsx
23
+ @mapping = mapping
24
+ @verbose = verbose
25
+ @cis_to_nist = get_cis_to_nist_control_mapping(CIS_2_NIST_XLSX)
26
+ end
27
+
28
+ def to_ckl
29
+ # TODO
30
+ end
31
+
32
+ def to_xccdf
33
+ # TODO
34
+ end
35
+
36
+ def to_inspec(control_prefix)
37
+ @controls = []
38
+ @cci_xml = nil
39
+ @profile = {}
40
+ insert_json_metadata
41
+ parse_cis_controls(control_prefix)
42
+ @profile['controls'] = @controls
43
+ @profile['sha256'] = Digest::SHA256.hexdigest @profile.to_s
44
+ @profile
45
+ end
46
+
47
+ private
48
+
49
+ def get_cis_to_nist_control_mapping(spreadsheet)
50
+ cis_to_nist = {}
51
+ spreadsheet.sheet(3).each do |row|
52
+ if row[3].is_a? Numeric
53
+ cis_to_nist[row[3].to_s] = row[0]
54
+ else
55
+ cis_to_nist[row[2].to_s] = row[0] unless (row[2] == '') || row[2].to_i.nil?
56
+ end
57
+ end
58
+ cis_to_nist
59
+ end
60
+
61
+ def insert_json_metadata
62
+ @profile['name'] = @name
63
+ @profile['title'] = 'InSpec Profile'
64
+ @profile['maintainer'] = 'The Authors'
65
+ @profile['copyright'] = 'The Authors'
66
+ @profile['copyright_email'] = 'you@example.com'
67
+ @profile['license'] = 'Apache-2.0'
68
+ @profile['summary'] = 'An InSpec Compliance Profile'
69
+ @profile['version'] = '0.1.0'
70
+ @profile['supports'] = []
71
+ @profile['attributes'] = []
72
+ @profile['generator'] = {
73
+ 'name': 'inspec_tools',
74
+ 'version': ::InspecTools::VERSION
75
+ }
76
+ end
77
+
78
+ def parse_cis_controls(control_prefix)
79
+ [1, 2].each do |level|
80
+ @xlsx.sheet(level).each_row_streaming do |row|
81
+ if row[@mapping['control.id']].nil? || !/^\d+(\.?\d)*$/.match(row[@mapping['control.id']].formatted_value)
82
+ next
83
+ end
84
+
85
+ tag_pos = @mapping['control.tags']
86
+ control = {}
87
+ control['tags'] = {}
88
+ control['id'] = control_prefix + '-' + row[@mapping['control.id']].formatted_value unless cell_empty?(@mapping['control.id']) || cell_empty?(row[@mapping['control.id']])
89
+ control['title'] = row[@mapping['control.title']].formatted_value unless cell_empty?(@mapping['control.title']) || cell_empty?(row[@mapping['control.title']])
90
+ control['desc'] = ''
91
+ control['desc'] = row[@mapping['control.desc']].formatted_value unless cell_empty?(row[@mapping['control.desc']])
92
+ control['tags']['rationale'] = row[tag_pos['rationale']].formatted_value unless cell_empty?(row[tag_pos['rationale']])
93
+
94
+ control['tags']['severity'] = level == 1 ? 'medium' : 'high'
95
+ control['impact'] = Utils::InspecUtil.get_impact(control['tags']['severity'])
96
+ control['tags']['ref'] = row[@mapping['control.ref']].formatted_value unless cell_empty?(@mapping['control.ref']) || cell_empty?(row[@mapping['control.ref']])
97
+ control['tags']['cis_level'] = level unless level.nil?
98
+
99
+ unless cell_empty?(row[tag_pos['cis_controls']])
100
+ # cis_control must be extracted from CIS control column via regex
101
+ cis_tags_array = row[tag_pos['cis_controls']].formatted_value.scan(/CONTROL:v(\d) (\d+)\.?(\d*)/).flatten
102
+ cis_tags = %i(revision section sub_section).zip(cis_tags_array).to_h
103
+ control = apply_cis_and_nist_controls(control, cis_tags)
104
+ end
105
+
106
+ control['tags']['cis_rid'] = row[@mapping['control.id']].formatted_value unless cell_empty?(@mapping['control.id']) || cell_empty?(row[@mapping['control.id']])
107
+ control['tags']['check'] = row[tag_pos['check']].formatted_value unless cell_empty?(tag_pos['check']) || cell_empty?(row[tag_pos['check']])
108
+ control['tags']['fix'] = row[tag_pos['fix']].formatted_value unless cell_empty?(tag_pos['fix']) || cell_empty?(row[tag_pos['fix']])
109
+
110
+ @controls << control
111
+ end
112
+ end
113
+ end
114
+
115
+ def cell_empty?(cell)
116
+ return cell.empty? if cell.respond_to?(:empty?)
117
+
118
+ cell.nil?
119
+ end
120
+
121
+ def apply_cis_and_nist_controls(control, cis_tags)
122
+ control['tags']['cis_controls'], control['tags']['nist'] = [], []
123
+
124
+ if cis_tags[:sub_section].nil? || cis_tags[:sub_section].blank?
125
+ control['tags']['cis_controls'] << cis_tags[:section]
126
+ control['tags']['nist'] << get_nist_control_for_cis(cis_tags[:section])
127
+ else
128
+ control['tags']['cis_controls'] << "#{cis_tags[:section]}.#{cis_tags[:sub_section]}"
129
+ control['tags']['nist'] << get_nist_control_for_cis(cis_tags[:section], cis_tags[:sub_section])
130
+ end
131
+
132
+ control['tags']['nist'] << LATEST_NIST_REV unless control['tags']['nist'].nil?
133
+ control['tags']['cis_controls'] << "Rev_#{cis_tags[:revision]}" unless cis_tags[:revision].nil?
134
+
135
+ control
136
+ end
137
+
138
+ def get_nist_control_for_cis(section, sub_section = nil)
139
+ return @cis_to_nist[section] if sub_section.nil?
140
+
141
+ @cis_to_nist["#{section}.#{sub_section}"]
142
+ end
143
+ end
144
+ end
145
+
146
+ # rubocop:enable Metrics/AbcSize
147
+ # rubocop:enable Metrics/PerceivedComplexity
148
+ # rubocop:enable Metrics/CyclomaticComplexity
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ libdir = File.dirname(__FILE__)
4
+ $LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir)
5
+
6
+ require 'inspec_tools/version'
7
+ require 'inspec_tools/plugin'
@@ -0,0 +1,5 @@
1
+ class FalseClass
2
+ def blank?
3
+ true
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class NilClass
2
+ def blank?
3
+ true
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class Object
2
+ def blank?
3
+ respond_to?(:empty?) ? empty? : !self
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class String
2
+ def blank?
3
+ self.strip.empty?
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class TrueClass
2
+ def blank?
3
+ false
4
+ end
5
+ end
@@ -0,0 +1,14 @@
1
+ require 'csv'
2
+
3
+ module Utils
4
+ class CSVUtil
5
+ def self.unpack_csv(csv_string, file)
6
+ csv = CSV.parse(csv_string)
7
+ CSV.open(file, 'wb') do |csv_file|
8
+ csv.each do |line|
9
+ csv_file << line
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,57 @@
1
+ require 'roo'
2
+
3
+ module Util
4
+ class ExtractNistMappings
5
+ def initialize(file)
6
+ @file = file
7
+ @full_excel = []
8
+ @headers = {}
9
+
10
+ open_excel
11
+ set_working_sheet
12
+ map_headers
13
+ retrieve_mappings
14
+ end
15
+
16
+ def open_excel
17
+ @xlsx = Roo::Excelx.new(@file)
18
+ end
19
+
20
+ def full_excl
21
+ @full_excel
22
+ end
23
+
24
+ def set_working_sheet
25
+ @xlsx.default_sheet = 'VER 6.1 Controls'
26
+ end
27
+
28
+ def map_headers
29
+ @xlsx.row(3).each_with_index { |header, i|
30
+ @headers[header] = i
31
+ }
32
+ end
33
+
34
+ def retrieve_mappings
35
+ nist_ver = 4
36
+ cis_ver = @xlsx.row(2)[4].split(' ')[-1]
37
+ ctrl_count = 1
38
+ ((@xlsx.first_row + 3)..@xlsx.last_row).each do |row_value|
39
+ current_row = {}
40
+ if @xlsx.row(row_value)[@headers['NIST SP 800-53 Control #']].to_s != ''
41
+ current_row[:nist] = @xlsx.row(row_value)[@headers['NIST SP 800-53 Control #']].to_s
42
+ else
43
+ current_row[:nist] = 'Not Mapped'
44
+ end
45
+ current_row[:nist_ver] = nist_ver
46
+ if @xlsx.row(row_value)[@headers['Control']].to_s == ''
47
+ current_row[:cis] = ctrl_count.to_s
48
+ ctrl_count += 1
49
+ else
50
+ current_row[:cis] = @xlsx.row(row_value)[@headers['Control']].to_s
51
+ end
52
+ current_row[:cis_ver] = cis_ver
53
+ @full_excel << current_row
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,20 @@
1
+ require 'pdf-reader'
2
+
3
+ module Util
4
+ class ExtractPdfText
5
+ def initialize(pdf)
6
+ @pdf = pdf
7
+ @extracted_text = ''
8
+ read_text
9
+ end
10
+
11
+ attr_reader :extracted_text
12
+
13
+ def read_text
14
+ reader = PDF::Reader.new(@pdf.path)
15
+ reader.pages.each do |page|
16
+ @extracted_text += page.text
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,435 @@
1
+ require 'inspec-objects'
2
+ require 'word_wrap'
3
+ require 'pp'
4
+ require 'uri'
5
+ require 'net/http'
6
+ require 'fileutils'
7
+ require 'exceptions/impact_input_error'
8
+ require 'exceptions/severity_input_error'
9
+ require 'overrides/false_class'
10
+ require 'overrides/true_class'
11
+ require 'overrides/nil_class'
12
+ require 'overrides/object'
13
+ require 'overrides/string'
14
+
15
+ # rubocop:disable Metrics/ClassLength
16
+ # rubocop:disable Metrics/AbcSize
17
+ # rubocop:disable Metrics/PerceivedComplexity
18
+ # rubocop:disable Metrics/CyclomaticComplexity
19
+ # rubocop:disable Metrics/MethodLength
20
+
21
+ module Utils
22
+ class InspecUtil
23
+ DATA_NOT_FOUND_MESSAGE = 'N/A'.freeze
24
+ WIDTH = 80
25
+ IMPACT_SCORES = {
26
+ 'none' => 0.0,
27
+ 'low' => 0.1,
28
+ 'medium' => 0.4,
29
+ 'high' => 0.7,
30
+ 'critical' => 0.9
31
+ }.freeze
32
+
33
+ def self.parse_data_for_xccdf(json)
34
+ data = {}
35
+
36
+ controls = []
37
+ if json['profiles'].nil?
38
+ controls = json['controls']
39
+ elsif json['profiles'].length == 1
40
+ controls = json['profiles'].last['controls']
41
+ else
42
+ json['profiles'].each do |profile|
43
+ controls.concat(profile['controls'])
44
+ end
45
+ end
46
+ c_data = {}
47
+
48
+ controls.each do |control|
49
+ c_id = control['id'].to_sym
50
+ c_data[c_id] = {}
51
+ c_data[c_id]['id'] = control['id'] || DATA_NOT_FOUND_MESSAGE
52
+ c_data[c_id]['title'] = control['title'] || DATA_NOT_FOUND_MESSAGE
53
+ c_data[c_id]['desc'] = control['desc'] || DATA_NOT_FOUND_MESSAGE
54
+ c_data[c_id]['severity'] = control['tags']['severity'] || DATA_NOT_FOUND_MESSAGE
55
+ c_data[c_id]['gid'] = control['tags']['gid'] || DATA_NOT_FOUND_MESSAGE
56
+ c_data[c_id]['gtitle'] = control['tags']['gtitle'] || DATA_NOT_FOUND_MESSAGE
57
+ c_data[c_id]['gdescription'] = control['tags']['gdescription'] || DATA_NOT_FOUND_MESSAGE
58
+ c_data[c_id]['rid'] = control['tags']['rid'] || DATA_NOT_FOUND_MESSAGE
59
+ c_data[c_id]['rversion'] = control['tags']['rversion'] || DATA_NOT_FOUND_MESSAGE
60
+ c_data[c_id]['rweight'] = control['tags']['rweight'] || DATA_NOT_FOUND_MESSAGE
61
+ c_data[c_id]['stig_id'] = control['tags']['stig_id'] || DATA_NOT_FOUND_MESSAGE
62
+ c_data[c_id]['cci'] = control['tags']['cci'] || DATA_NOT_FOUND_MESSAGE
63
+ c_data[c_id]['nist'] = control['tags']['nist'] || ['unmapped']
64
+ c_data[c_id]['check'] = control['tags']['check'] || DATA_NOT_FOUND_MESSAGE
65
+ c_data[c_id]['checkref'] = control['tags']['checkref'] || DATA_NOT_FOUND_MESSAGE
66
+ c_data[c_id]['fix'] = control['tags']['fix'] || DATA_NOT_FOUND_MESSAGE
67
+ c_data[c_id]['fixref'] = control['tags']['fixref'] || DATA_NOT_FOUND_MESSAGE
68
+ c_data[c_id]['fix_id'] = control['tags']['fix_id'] || DATA_NOT_FOUND_MESSAGE
69
+ c_data[c_id]['rationale'] = control['tags']['rationale'] || DATA_NOT_FOUND_MESSAGE
70
+ c_data[c_id]['cis_family'] = control['tags']['cis_family'] || DATA_NOT_FOUND_MESSAGE
71
+ c_data[c_id]['cis_rid'] = control['tags']['cis_rid'] || DATA_NOT_FOUND_MESSAGE
72
+ c_data[c_id]['cis_level'] = control['tags']['cis_level'] || DATA_NOT_FOUND_MESSAGE
73
+ c_data[c_id]['impact'] = control['impact'].to_s || DATA_NOT_FOUND_MESSAGE
74
+ c_data[c_id]['code'] = control['code'].to_s || DATA_NOT_FOUND_MESSAGE
75
+ end
76
+
77
+ data['controls'] = c_data.values
78
+ data['status'] = 'success'
79
+ data
80
+ end
81
+
82
+ def self.parse_data_for_ckl(json)
83
+ data = {}
84
+
85
+ # Parse for inspec profile results json
86
+ json['profiles'].each do |profile|
87
+ profile['controls'].each do |control|
88
+ c_id = control['id'].to_sym
89
+ data[c_id] = {}
90
+
91
+ data[c_id][:vuln_num] = control['id'] unless control['id'].nil?
92
+ data[c_id][:rule_title] = control['title'] unless control['title'].nil?
93
+ data[c_id][:vuln_discuss] = control['desc'] unless control['desc'].nil?
94
+
95
+ unless control['tags'].nil?
96
+ data[c_id][:severity] = control['tags']['severity'] unless control['tags']['severity'].nil?
97
+ data[c_id][:gid] = control['tags']['gid'] unless control['tags']['gid'].nil?
98
+ data[c_id][:group_title] = control['tags']['gtitle'] unless control['tags']['gtitle'].nil?
99
+ data[c_id][:rule_id] = control['tags']['rid'] unless control['tags']['rid'].nil?
100
+ data[c_id][:rule_ver] = control['tags']['stig_id'] unless control['tags']['stig_id'].nil?
101
+ data[c_id][:cci_ref] = control['tags']['cci'] unless control['tags']['cci'].nil?
102
+ data[c_id][:nist] = control['tags']['nist'].join(' ') unless control['tags']['nist'].nil?
103
+ end
104
+
105
+ if control['descriptions'].respond_to?(:find)
106
+ data[c_id][:check_content] = control['descriptions'].find { |c| c['label'] == 'fix' }&.dig('data')
107
+ data[c_id][:fix_text] = control['descriptions'].find { |c| c['label'] == 'check' }&.dig('data')
108
+ end
109
+
110
+ data[c_id][:impact] = control['impact'].to_s unless control['impact'].nil?
111
+ data[c_id][:profile_name] = profile['name'].to_s unless profile['name'].nil?
112
+ data[c_id][:profile_shasum] = profile['sha256'].to_s unless profile['sha256'].nil?
113
+
114
+ data[c_id][:status] = []
115
+ data[c_id][:message] = []
116
+
117
+ if control.key?('results')
118
+ control['results'].each do |result|
119
+ if !result['backtrace'].nil?
120
+ result['status'] = 'error'
121
+ end
122
+ data[c_id][:status].push(result['status'])
123
+ data[c_id][:message].push("SKIPPED -- Test: #{result['code_desc']}\nMessage: #{result['skip_message']}\n") if result['status'] == 'skipped'
124
+ data[c_id][:message].push("FAILED -- Test: #{result['code_desc']}\nMessage: #{result['message']}\n") if result['status'] == 'failed'
125
+ data[c_id][:message].push("PASS -- #{result['code_desc']}\n") if result['status'] == 'passed'
126
+ data[c_id][:message].push("PROFILE_ERROR -- Test: #{result['code_desc']}\nMessage: #{result['backtrace']}\n") if result['status'] == 'error'
127
+ end
128
+ end
129
+
130
+ if data[c_id][:impact].to_f.zero?
131
+ data[c_id][:message].unshift("NOT_APPLICABLE -- Description: #{control['desc']}\n\n")
132
+ end
133
+ end
134
+ end
135
+ data
136
+ end
137
+
138
+ def self.get_platform(json)
139
+ json['profiles'].find { |profile| !profile[:platform].nil? }
140
+ end
141
+
142
+ def self.to_dotted_hash(hash, recursive_key = '')
143
+ hash.each_with_object({}) do |(k, v), ret|
144
+ key = recursive_key + k.to_s
145
+ if v.is_a? Hash
146
+ ret.merge! to_dotted_hash(v, key + '.')
147
+ else
148
+ ret[key] = v
149
+ end
150
+ end
151
+ end
152
+
153
+ def self.control_status(control, for_summary = false)
154
+ status_list = control[:status].uniq
155
+ if control[:impact].to_f.zero?
156
+ 'Not_Applicable'
157
+ elsif status_list.include?('failed')
158
+ 'Open'
159
+ elsif status_list.include?('passed')
160
+ 'NotAFinding'
161
+ elsif status_list.include?('error') && for_summary
162
+ 'Profile_Error'
163
+ else
164
+ # profile skipped or profile error
165
+ 'Not_Reviewed'
166
+ end
167
+ end
168
+
169
+ def self.control_finding_details(control, control_clk_status)
170
+ result = "One or more of the automated tests failed or was inconclusive for the control \n\n #{control[:message].sort.join}" if control_clk_status == 'Open'
171
+ result = "All Automated tests passed for the control \n\n #{control[:message].join}" if control_clk_status == 'NotAFinding'
172
+ result = "Automated test skipped due to known accepted condition in the control : \n\n#{control[:message].join}" if control_clk_status == 'Not_Reviewed'
173
+ result = "Justification: \n #{control[:message].join}" if control_clk_status == 'Not_Applicable'
174
+ result = 'No test available or some test errors occurred for this control' if control_clk_status == 'Profile_Error'
175
+ result
176
+ end
177
+
178
+ # @!method get_impact(severity)
179
+ # Takes in the STIG severity tag and converts it to the InSpec #{impact}
180
+ # control tag.
181
+ # At the moment the mapping is static, so that:
182
+ # high => 0.7
183
+ # medium => 0.5
184
+ # low => 0.3
185
+ # @param severity [String] the string value you want to map to an InSpec
186
+ # 'impact' level.
187
+ #
188
+ # @return impact [Float] the impact level level mapped to the XCCDF severity
189
+ # mapped to a float between 0.0 - 1.0.
190
+ #
191
+ # @todo Allow for the user to pass in a hash for the desired mapping of text
192
+ # values to numbers or to override our hard coded values.
193
+ #
194
+ def self.get_impact(severity, use_cvss_terms: true)
195
+ return float_to_impact(severity, use_cvss_terms) if severity.is_a?(Float)
196
+
197
+ return string_to_impact(severity, use_cvss_terms) if severity.is_a?(String)
198
+
199
+ raise SeverityInputError, "'#{severity}' is not a valid severity value. It should be a Float between 0.0 and " \
200
+ '1.0 or one of the approved keywords.'
201
+ end
202
+
203
+ private_class_method def self.float_to_impact(severity, use_cvss_terms)
204
+ unless severity.between?(0, 1)
205
+ raise SeverityInputError, "'#{severity}' is not a valid severity value. It should be a Float between 0.0 and " \
206
+ '1.0 or one of the approved keywords.'
207
+ end
208
+
209
+ if severity <= 0.01
210
+ 0.0 # Informative
211
+ elsif severity < 0.4
212
+ 0.3 # Low Impact
213
+ elsif severity < 0.7
214
+ 0.5 # Medium Impact
215
+ elsif severity < 0.9 || use_cvss_terms
216
+ 0.7 # High Impact
217
+ else
218
+ 1.0 # Critical Controls
219
+ end
220
+ end
221
+
222
+ private_class_method def self.string_to_impact(severity, use_cvss_terms)
223
+ if /none|na|n\/a|not[_|(\s*)]?applicable/i.match?(severity)
224
+ impact = 0.0 # Informative
225
+ elsif /low|cat(egory)?\s*(iii|3)/i.match?(severity)
226
+ impact = 0.3 # Low Impact
227
+ elsif /med(ium)?|cat(egory)?\s*(ii|2)/i.match?(severity)
228
+ impact = 0.5 # Medium Impact
229
+ elsif /high|cat(egory)?\s*(i|1)/i.match?(severity)
230
+ impact = 0.7 # High Impact
231
+ elsif /crit(ical)?|severe/i.match?(severity)
232
+ impact = 1.0 # Critical Controls
233
+ else
234
+ raise SeverityInputError, "'#{severity}' is not a valid severity value. It should be a Float between 0.0 and " \
235
+ '1.0 or one of the approved keywords.'
236
+ end
237
+
238
+ impact == 1.0 && use_cvss_terms ? 0.7 : impact
239
+ end
240
+
241
+ def self.get_impact_string(impact, use_cvss_terms: true)
242
+ return if impact.nil?
243
+
244
+ value = impact.to_f
245
+ unless value.between?(0, 1)
246
+ raise ImpactInputError, "'#{value}' is not a valid impact score. Valid impact scores: [0.0 - 1.0]."
247
+ end
248
+
249
+ IMPACT_SCORES.reverse_each do |name, impact_score|
250
+ if name == 'critical' && value >= impact_score && use_cvss_terms
251
+ return 'high'
252
+ elsif value >= impact_score
253
+ return name
254
+ else
255
+ next
256
+ end
257
+ end
258
+ end
259
+
260
+ def self.unpack_inspec_json(directory, inspec_json, separated, output_format)
261
+ if directory == 'id'
262
+ directory = inspec_json['name']
263
+ end
264
+ controls = generate_controls(inspec_json)
265
+ unpack_profile(directory || 'profile', controls, separated, output_format || 'json')
266
+ create_inspec_yml(directory || 'profile', inspec_json)
267
+ create_license(directory || 'profile', inspec_json)
268
+ create_readme_md(directory || 'profile', inspec_json)
269
+ end
270
+
271
+ private_class_method def self.wrap(str, width = WIDTH)
272
+ str.gsub!("desc \"\n ", 'desc "')
273
+ str.gsub!(/\\r/, "\n")
274
+ str.gsub!(/\\n/, "\n")
275
+
276
+ WordWrap.ww(str.to_s, width)
277
+ end
278
+
279
+ private_class_method def self.generate_controls(inspec_json)
280
+ controls = []
281
+ inspec_json['controls'].each do |json_control|
282
+ control = ::Inspec::Object::Control.new
283
+ if (defined? control.desc).nil?
284
+ control.descriptions[:default] = json_control['desc']
285
+ control.descriptions[:rationale] = json_control['tags']['rationale']
286
+ control.descriptions[:check] = json_control['tags']['check']
287
+ control.descriptions[:fix] = json_control['tags']['fix']
288
+ else
289
+ control.desc = json_control['desc']
290
+ end
291
+ control.id = json_control['id']
292
+ control.title = json_control['title']
293
+ control.impact = get_impact(json_control['impact'])
294
+
295
+ # json_control['tags'].each do |tag|
296
+ # control.add_tag(Inspec::Object::Tag.new(tag.key, tag.value)
297
+ # end
298
+
299
+ control.add_tag(::Inspec::Object::Tag.new('severity', json_control['tags']['severity']))
300
+ control.add_tag(::Inspec::Object::Tag.new('gtitle', json_control['tags']['gtitle']))
301
+ control.add_tag(::Inspec::Object::Tag.new('satisfies', json_control['tags']['satisfies'])) if json_control['tags']['satisfies']
302
+ control.add_tag(::Inspec::Object::Tag.new('gid', json_control['tags']['gid']))
303
+ control.add_tag(::Inspec::Object::Tag.new('rid', json_control['tags']['rid']))
304
+ control.add_tag(::Inspec::Object::Tag.new('stig_id', json_control['tags']['stig_id']))
305
+ control.add_tag(::Inspec::Object::Tag.new('fix_id', json_control['tags']['fix_id']))
306
+ control.add_tag(::Inspec::Object::Tag.new('cci', json_control['tags']['cci']))
307
+ control.add_tag(::Inspec::Object::Tag.new('nist', json_control['tags']['nist']))
308
+ control.add_tag(::Inspec::Object::Tag.new('cis_level', json_control['tags']['cis_level'])) unless json_control['tags']['cis_level'].blank?
309
+ control.add_tag(::Inspec::Object::Tag.new('cis_controls', json_control['tags']['cis_controls'])) unless json_control['tags']['cis_controls'].blank?
310
+ control.add_tag(::Inspec::Object::Tag.new('cis_rid', json_control['tags']['cis_rid'])) unless json_control['tags']['cis_rid'].blank?
311
+ control.add_tag(::Inspec::Object::Tag.new('ref', json_control['tags']['ref'])) unless json_control['tags']['ref'].blank?
312
+ control.add_tag(::Inspec::Object::Tag.new('false_negatives', json_control['tags']['false_negatives'])) unless json_control['tags']['false_positives'].blank?
313
+ control.add_tag(::Inspec::Object::Tag.new('false_positives', json_control['tags']['false_positives'])) unless json_control['tags']['false_positives'].blank?
314
+ control.add_tag(::Inspec::Object::Tag.new('documentable', json_control['tags']['documentable'])) unless json_control['tags']['documentable'].blank?
315
+ control.add_tag(::Inspec::Object::Tag.new('mitigations', json_control['tags']['mitigations'])) unless json_control['tags']['mitigations'].blank?
316
+ control.add_tag(::Inspec::Object::Tag.new('severity_override_guidance', json_control['tags']['severity_override_guidance'])) unless json_control['tags']['severity_override_guidance'].blank?
317
+ control.add_tag(::Inspec::Object::Tag.new('potential_impacts', json_control['tags']['potential_impacts'])) unless json_control['tags']['potential_impacts'].blank?
318
+ control.add_tag(::Inspec::Object::Tag.new('third_party_tools', json_control['tags']['third_party_tools'])) unless json_control['tags']['third_party_tools'].blank?
319
+ control.add_tag(::Inspec::Object::Tag.new('mitigation_controls', json_control['tags']['mitigation_controls'])) unless json_control['tags']['mitigation_controls'].blank?
320
+ control.add_tag(::Inspec::Object::Tag.new('responsibility', json_control['tags']['responsibility'])) unless json_control['tags']['responsibility'].blank?
321
+ control.add_tag(::Inspec::Object::Tag.new('ia_controls', json_control['tags']['ia_controls'])) unless json_control['tags']['ia_controls'].blank?
322
+
323
+ controls << control
324
+ end
325
+ controls
326
+ end
327
+
328
+ # @!method print_benchmark_info(info)
329
+ # writes benchmark info to profile inspec.yml file
330
+ #
331
+ private_class_method def self.create_inspec_yml(directory, inspec_json)
332
+ benchmark_info =
333
+ "name: #{inspec_json['name']}\n" \
334
+ "title: #{inspec_json['title']}\n" \
335
+ "maintainer: #{inspec_json['maintainer']}\n" \
336
+ "copyright: #{inspec_json['copyright']}\n" \
337
+ "copyright_email: #{inspec_json['copyright_email']}\n" \
338
+ "license: #{inspec_json['license']}\n" \
339
+ "summary: #{inspec_json['summary']}\n" \
340
+ "version: #{inspec_json['version']}\n"
341
+
342
+ myfile = File.new("#{directory}/inspec.yml", 'w')
343
+ myfile.puts benchmark_info
344
+ end
345
+
346
+ private_class_method def self.create_license(directory, inspec_json)
347
+ license_content = ''
348
+ if !inspec_json['license'].nil?
349
+ begin
350
+ response = Net::HTTP.get_response(URI(inspec_json['license']))
351
+ if response.code == '200'
352
+ license_content = response.body
353
+ else
354
+ license_content = inspec_json['license']
355
+ end
356
+ rescue StandardError => _e
357
+ license_content = inspec_json['license']
358
+ end
359
+ end
360
+
361
+ myfile = File.new("#{directory}/LICENSE", 'w')
362
+ myfile.puts license_content
363
+ end
364
+
365
+ private_class_method def self.create_readme_md(directory, inspec_json)
366
+ readme_contents =
367
+ "\# #{inspec_json['title']}\n" \
368
+ "#{inspec_json['summary']}\n" \
369
+ "---\n" \
370
+ "Name: #{inspec_json['name']}\n" \
371
+ "Author: #{inspec_json['maintainer']}\n" \
372
+ "Status: #{inspec_json['status']}\n" \
373
+ "Copyright: #{inspec_json['copyright']}\n" \
374
+ "Copyright Email: #{inspec_json['copyright_email']}\n" \
375
+ "Version: #{inspec_json['version']}\n" \
376
+ "#{inspec_json['plaintext']}\n" \
377
+ "Reference: #{inspec_json['reference_href']}\n" \
378
+ "Reference by: #{inspec_json['reference_publisher']}\n" \
379
+ "Reference source: #{inspec_json['reference_source']}\n"
380
+
381
+ myfile = File.new("#{directory}/README.md", 'w')
382
+ myfile.puts readme_contents
383
+ end
384
+
385
+ private_class_method def self.unpack_profile(directory, controls, separated, output_format)
386
+ FileUtils.rm_rf(directory) if Dir.exist?(directory)
387
+ Dir.mkdir directory unless Dir.exist?(directory)
388
+ Dir.mkdir "#{directory}/controls" unless Dir.exist?("#{directory}/controls")
389
+ Dir.mkdir "#{directory}/libraries" unless Dir.exist?("#{directory}/libraries")
390
+ if separated
391
+ if output_format == 'ruby'
392
+ controls.each do |control|
393
+ file_name = control.id.to_s
394
+ myfile = File.new("#{directory}/controls/#{file_name}.rb", 'w')
395
+ myfile.puts "# encoding: UTF-8\n\n"
396
+ myfile.puts wrap(control.to_ruby.gsub('"', "\'"), WIDTH) + "\n"
397
+ myfile.close
398
+ end
399
+ else
400
+ controls.each do |control|
401
+ file_name = control.id.to_s
402
+ myfile = File.new("#{directory}/controls/#{file_name}.rb", 'w')
403
+ PP.pp(control.to_hash, myfile)
404
+ myfile.close
405
+ end
406
+ end
407
+ else
408
+ myfile = File.new("#{directory}/controls/controls.rb", 'w')
409
+ if output_format == 'ruby'
410
+ controls.each do |control|
411
+ myfile.puts "# encoding: UTF-8\n\n"
412
+ myfile.puts wrap(control.to_ruby.gsub('"', "\'"), WIDTH) + "\n"
413
+ end
414
+ else
415
+ controls.each do |control|
416
+ if (defined? control.desc).nil?
417
+ control.descriptions[:default].strip!
418
+ else
419
+ control.desc.strip!
420
+ end
421
+
422
+ PP.pp(control.to_hash, myfile)
423
+ end
424
+ end
425
+ myfile.close
426
+ end
427
+ end
428
+ end
429
+ end
430
+
431
+ # rubocop:enable Metrics/ClassLength
432
+ # rubocop:enable Metrics/AbcSize
433
+ # rubocop:enable Metrics/PerceivedComplexity
434
+ # rubocop:enable Metrics/CyclomaticComplexity
435
+ # rubocop:enable Metrics/MethodLength