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,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,278 @@
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 :output, required: false, aliases: '-o'
204
+ option :cli, type: :boolean, required: false, aliases: '-c'
205
+ option :verbose, type: :boolean, aliases: '-V'
206
+ option :json_full, type: :boolean, required: false, aliases: '-f'
207
+ option :json_counts, type: :boolean, required: false, aliases: '-k'
208
+
209
+ def summary
210
+ summary = InspecTools::Summary.new(File.read(options[:inspec_json])).to_summary
211
+
212
+ if options[:cli]
213
+ puts "\nOverall compliance: #{summary[:compliance]}%\n\n"
214
+ summary[:status].keys.each do |category|
215
+ puts category
216
+ summary[:status][category].keys.each do |impact|
217
+ puts "\t#{impact} : #{summary[:status][category][impact]}"
218
+ end
219
+ end
220
+ end
221
+
222
+ json_summary = summary.to_json
223
+ File.write(options[:output], json_summary) if options[:output]
224
+ puts json_summary if options[:json_full]
225
+ puts summary[:status].to_json if options[:json_counts]
226
+ end
227
+
228
+ desc 'compliance', 'compliance parses an inspec results json to check if the compliance level meets a specified threshold'
229
+ long_desc InspecTools::Help.text(:compliance)
230
+ option :inspec_json, required: true, aliases: '-j'
231
+ option :threshold_file, required: false, aliases: '-f'
232
+ option :threshold_inline, required: false, aliases: '-i'
233
+ option :verbose, type: :boolean, aliases: '-V'
234
+
235
+ def compliance
236
+ if options[:threshold_file].nil? && options[:threshold_inline].nil?
237
+ puts 'Please provide threshold as a yaml file or inline yaml'
238
+ exit(1)
239
+ end
240
+ threshold = YAML.load_file(options[:threshold_file]) unless options[:threshold_file].nil?
241
+ threshold = YAML.safe_load(options[:threshold_inline]) unless options[:threshold_inline].nil?
242
+ compliance = InspecTools::Summary.new(File.read(options[:inspec_json])).threshold(threshold)
243
+ compliance ? exit(0) : exit(1)
244
+ end
245
+ end
246
+ end
247
+ end
248
+
249
+ #=====================================================================#
250
+ # Pre-Flight Code
251
+ #=====================================================================#
252
+ help_commands = ['-h', '--help', 'help']
253
+ log_commands = ['-l', '--log-directory']
254
+ version_commands = ['-v', '--version', 'version']
255
+
256
+ #---------------------------------------------------------------------#
257
+ # Adjustments for non-required version commands
258
+ #---------------------------------------------------------------------#
259
+ unless (version_commands & ARGV).empty?
260
+ puts InspecTools::VERSION
261
+ exit 0
262
+ end
263
+
264
+ #---------------------------------------------------------------------#
265
+ # Adjustments for non-required log-directory
266
+ #---------------------------------------------------------------------#
267
+ ARGV.push("--log-directory=#{Dir.pwd}/logs") if (log_commands & ARGV).empty? && (help_commands & ARGV).empty?
268
+
269
+ # Push help to front of command so thor recognizes subcommands are called with help
270
+ if help_commands.any? { |cmd| ARGV.include? cmd }
271
+ help_commands.each do |cmd|
272
+ if (match = ARGV.delete(cmd))
273
+ ARGV.unshift match
274
+ end
275
+ end
276
+ end
277
+
278
+ # 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 = %w{failed passed no_impact skipped error}.freeze
14
+ TALLYS = %w{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])).round(1)
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.to_sym][tally.to_sym] > max
108
+ compliance = false
109
+ failure << "Expected #{bucket}.#{tally}.max:#{max} got:#{status[bucket.to_sym][tally.to_sym]}"
110
+ end
111
+ if min != -1 and status[bucket.to_sym][tally.to_sym] < min
112
+ compliance = false
113
+ failure << "Expected #{bucket}.#{tally}.min:#{min} got:#{status[bucket.to_sym][tally.to_sym]}"
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
@@ -0,0 +1,8 @@
1
+ require 'git-version-bump'
2
+
3
+ module InspecTools
4
+ # Enable lite-tags (2nd parameter to git-version-bump version command)
5
+ # Lite tags are tags that are used by GitHub releases that do not contain
6
+ # annotations
7
+ VERSION = GVB.version(false, true)
8
+ end
@@ -0,0 +1,155 @@
1
+ require_relative '../happy_mapper_tools/stig_attributes'
2
+ require_relative '../happy_mapper_tools/cci_attributes'
3
+ require_relative '../utilities/inspec_util'
4
+
5
+ require 'digest'
6
+ require 'json'
7
+
8
+ module InspecTools
9
+ # rubocop:disable Metrics/ClassLength
10
+ # rubocop:disable Metrics/AbcSize
11
+ # rubocop:disable Metrics/PerceivedComplexity
12
+ # rubocop:disable Metrics/CyclomaticComplexity
13
+ # rubocop:disable Metrics/BlockLength
14
+ class XCCDF
15
+ def initialize(xccdf, replace_tags = nil)
16
+ @xccdf = xccdf
17
+ @xccdf = replace_tags_in_xccdf(replace_tags, @xccdf) unless replace_tags.nil?
18
+ cci_list_path = File.join(File.dirname(__FILE__), '../data/U_CCI_List.xml')
19
+ @cci_items = HappyMapperTools::CCIAttributes::CCI_List.parse(File.read(cci_list_path))
20
+ # @cci_items = HappyMapperTools::CCIAttributes::CCI_List.parse(File.read('./data/U_CCI_List.xml'))
21
+ @benchmark = HappyMapperTools::StigAttributes::Benchmark.parse(@xccdf)
22
+ end
23
+
24
+ def to_ckl
25
+ # TODO: to_ckl
26
+ end
27
+
28
+ def to_csv
29
+ # TODO: to_csv
30
+ end
31
+
32
+ def to_inspec
33
+ @profile = {}
34
+ @controls = []
35
+ insert_json_metadata
36
+ insert_controls
37
+ @profile['sha256'] = Digest::SHA256.hexdigest @profile.to_s
38
+ @profile
39
+ end
40
+
41
+ ####
42
+ # extracts non-InSpec attributes
43
+ ###
44
+ # TODO there may be more attributes we want to extract, see data/attributes.yml for example
45
+ def to_attributes # rubocop:disable Metrics/AbcSize
46
+ @attribute = {}
47
+
48
+ @attribute['benchmark.title'] = @benchmark.title
49
+ @attribute['benchmark.id'] = @benchmark.id
50
+ @attribute['benchmark.description'] = @benchmark.description
51
+ @attribute['benchmark.version'] = @benchmark.version
52
+
53
+ @attribute['benchmark.status'] = @benchmark.status
54
+ @attribute['benchmark.status.date'] = @benchmark.release_date.release_date
55
+
56
+ @attribute['benchmark.notice.id'] = @benchmark.notice.id
57
+
58
+ @attribute['benchmark.plaintext'] = @benchmark.plaintext.plaintext
59
+ @attribute['benchmark.plaintext.id'] = @benchmark.plaintext.id
60
+
61
+ @attribute['reference.href'] = @benchmark.reference.href
62
+ @attribute['reference.dc.publisher'] = @benchmark.reference.dc_publisher
63
+ @attribute['reference.dc.source'] = @benchmark.reference.dc_source
64
+ @attribute['reference.dc.title'] = @benchmark.group[0].rule.reference.dc_title if !@benchmark.group[0].nil?
65
+ @attribute['reference.dc.subject'] = @benchmark.group[0].rule.reference.dc_subject if !@benchmark.group[0].nil?
66
+ @attribute['reference.dc.type'] = @benchmark.group[0].rule.reference.dc_type if !@benchmark.group[0].nil?
67
+ @attribute['reference.dc.identifier'] = @benchmark.group[0].rule.reference.dc_identifier if !@benchmark.group[0].nil?
68
+
69
+ @attribute['content_ref.name'] = @benchmark.group[0].rule.check.content_ref.name if !@benchmark.group[0].nil?
70
+ @attribute['content_ref.href'] = @benchmark.group[0].rule.check.content_ref.href if !@benchmark.group[0].nil?
71
+
72
+ @attribute
73
+ end
74
+
75
+ def publisher
76
+ @benchmark.reference.dc_publisher
77
+ end
78
+
79
+ def published
80
+ @benchmark.release_date.release_date
81
+ end
82
+
83
+ def inject_metadata(metadata = '{}')
84
+ json_metadata = JSON.parse(metadata)
85
+ json_metadata.each do |key, value|
86
+ @profile[key] = value
87
+ end
88
+ end
89
+
90
+ private
91
+
92
+ def replace_tags_in_xccdf(replace_tags, xccdf_xml)
93
+ replace_tags.each do |tag|
94
+ xccdf_xml = xccdf_xml.gsub(/(&lt;|<)#{tag}(&gt;|>)/, "$#{tag}")
95
+ end
96
+ xccdf_xml
97
+ end
98
+
99
+ def insert_json_metadata
100
+ @profile['name'] = @benchmark.id
101
+ @profile['title'] = @benchmark.title
102
+ @profile['maintainer'] = 'The Authors' if @profile['maintainer'].nil?
103
+ @profile['copyright'] = 'The Authors' if @profile['copyright'].nil?
104
+ @profile['copyright_email'] = 'you@example.com' if @profile['copyright_email'].nil?
105
+ @profile['license'] = 'Apache-2.0' if @profile['license'].nil?
106
+ @profile['summary'] = "\"#{@benchmark.description.gsub('\\', '\\\\\\').gsub('"', '\"')}\""
107
+ @profile['version'] = '0.1.0' if @profile['version'].nil?
108
+ @profile['supports'] = []
109
+ @profile['attributes'] = []
110
+ @profile['generator'] = {
111
+ 'name': 'inspec_tools',
112
+ 'version': VERSION
113
+ }
114
+ @profile['plaintext'] = @benchmark.plaintext.plaintext
115
+ @profile['status'] = "#{@benchmark.status} on #{@benchmark.release_date.release_date}"
116
+ @profile['reference_href'] = @benchmark.reference.href
117
+ @profile['reference_publisher'] = @benchmark.reference.dc_publisher
118
+ @profile['reference_source'] = @benchmark.reference.dc_source
119
+ end
120
+
121
+ def insert_controls
122
+ @benchmark.group.each do |group|
123
+ control = {}
124
+ control['id'] = group.id
125
+ control['title'] = group.rule.title
126
+ control['desc'] = group.rule.description.vuln_discussion.split('Satisfies: ')[0]
127
+ control['impact'] = Utils::InspecUtil.get_impact(group.rule.severity)
128
+ control['tags'] = {}
129
+ control['tags']['severity'] = Utils::InspecUtil.get_impact_string(control['impact'])
130
+ control['tags']['gtitle'] = group.title
131
+ control['tags']['satisfies'] = group.rule.description.vuln_discussion.split('Satisfies: ')[1].split(',').map(&:strip) if group.rule.description.vuln_discussion.split('Satisfies: ').length > 1
132
+ control['tags']['gid'] = group.id
133
+ control['tags']['rid'] = group.rule.id
134
+ control['tags']['stig_id'] = group.rule.version
135
+ control['tags']['fix_id'] = group.rule.fix.id
136
+ control['tags']['cci'] = group.rule.idents
137
+ control['tags']['nist'] = @cci_items.fetch_nists(group.rule.idents)
138
+ control['tags']['false_negatives'] = group.rule.description.false_negatives if group.rule.description.false_negatives != ''
139
+ control['tags']['false_positives'] = group.rule.description.false_positives if group.rule.description.false_positives != ''
140
+ control['tags']['documentable'] = group.rule.description.documentable if group.rule.description.documentable != ''
141
+ control['tags']['mitigations'] = group.rule.description.false_negatives if group.rule.description.mitigations != ''
142
+ control['tags']['severity_override_guidance'] = group.rule.description.severity_override_guidance if group.rule.description.severity_override_guidance != ''
143
+ control['tags']['potential_impacts'] = group.rule.description.potential_impacts if group.rule.description.potential_impacts != ''
144
+ control['tags']['third_party_tools'] = group.rule.description.third_party_tools if group.rule.description.third_party_tools != ''
145
+ control['tags']['mitigation_controls'] = group.rule.description.mitigation_controls if group.rule.description.mitigation_controls != ''
146
+ control['tags']['responsibility'] = group.rule.description.responsibility if group.rule.description.responsibility != ''
147
+ control['tags']['ia_controls'] = group.rule.description.ia_controls if group.rule.description.ia_controls != ''
148
+ control['tags']['check'] = group.rule.check.content
149
+ control['tags']['fix'] = group.rule.fixtext
150
+ @controls << control
151
+ end
152
+ @profile['controls'] = @controls
153
+ end
154
+ end
155
+ end