inspec_tools 0.0.0.1.ENOTAG

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/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