inspec_tools 2.0.4 → 2.2.0

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.
@@ -1,10 +1,10 @@
1
1
  require 'csv'
2
- require 'nokogiri'
3
- require 'word_wrap'
4
2
  require 'yaml'
5
3
  require 'digest'
6
4
 
7
5
  require_relative '../utilities/inspec_util'
6
+ require_relative '../utilities/cci_xml'
7
+ require_relative '../utilities/mapping_validator'
8
8
 
9
9
  # rubocop:disable Metrics/AbcSize
10
10
  # rubocop:disable Metrics/PerceivedComplexity
@@ -16,34 +16,25 @@ module InspecTools
16
16
  def initialize(csv, mapping, name, verbose = false)
17
17
  @name = name
18
18
  @csv = csv
19
- @mapping = mapping
19
+ @mapping = Utils::MappingValidator.validate(mapping)
20
20
  @verbose = verbose
21
21
  @csv.shift if @mapping['skip_csv_header']
22
22
  end
23
23
 
24
- def to_ckl
25
- # TODO
26
- end
27
-
28
- def to_xccdf
29
- # TODO
30
- end
31
-
32
- def to_inspec
24
+ def to_inspec(control_name_prefix: nil)
33
25
  @controls = []
34
- @cci_xml = nil
35
26
  @profile = {}
36
- read_cci_xml
37
- insert_json_metadata
38
- parse_controls
27
+ @cci_xml = Utils::CciXml.get_cci_list('U_CCI_List.xml')
28
+ insert_metadata
29
+ parse_controls(control_name_prefix)
39
30
  @profile['controls'] = @controls
40
- @profile['sha256'] = Digest::SHA256.hexdigest @profile.to_s
31
+ @profile['sha256'] = Digest::SHA256.hexdigest(@profile.to_s)
41
32
  @profile
42
33
  end
43
34
 
44
35
  private
45
36
 
46
- def insert_json_metadata
37
+ def insert_metadata
47
38
  @profile['name'] = @name
48
39
  @profile['title'] = 'InSpec Profile'
49
40
  @profile['maintainer'] = 'The Authors'
@@ -60,35 +51,37 @@ module InspecTools
60
51
  }
61
52
  end
62
53
 
63
- def read_cci_xml
64
- cci_list_path = File.join(File.dirname(__FILE__), '../data/U_CCI_List.xml')
65
- @cci_xml = Nokogiri::XML(File.open(cci_list_path))
66
- @cci_xml.remove_namespaces!
67
- rescue StandardError => e
68
- puts "Exception: #{e.message}"
69
- end
70
-
71
54
  def get_nist_reference(cci_number)
72
55
  item_node = @cci_xml.xpath("//cci_list/cci_items/cci_item[@id='#{cci_number}']")[0] unless @cci_xml.nil?
73
- unless item_node.nil?
74
- nist_ref = item_node.xpath('./references/reference[not(@version <= preceding-sibling::reference/@version) and not(@version <=following-sibling::reference/@version)]/@index').text
75
- nist_ver = item_node.xpath('./references/reference[not(@version <= preceding-sibling::reference/@version) and not(@version <=following-sibling::reference/@version)]/@version').text
76
- end
77
- [nist_ref, nist_ver]
56
+ return nil if item_node.nil?
57
+
58
+ [] << item_node.xpath('./references/reference[not(@version <= preceding-sibling::reference/@version) and not(@version <=following-sibling::reference/@version)]/@index').text
78
59
  end
79
60
 
80
- def parse_controls
61
+ def get_cci_number(cell)
62
+ # Return nil if a mapping to the CCI was not provided or if there is not content in the CSV cell.
63
+ return nil if cell.nil? || @mapping['control.tags']['cci'].nil?
64
+
65
+ # If the content has been exported from STIG Viewer, the cell will have extra information
66
+ cell.split("\n").first
67
+ end
68
+
69
+ def parse_controls(prefix)
81
70
  @csv.each do |row|
82
- print '.'
83
71
  control = {}
84
- control['id'] = row[@mapping['control.id']] unless @mapping['control.id'].nil? || row[@mapping['control.id']].nil?
85
- control['title'] = row[@mapping['control.title']] unless @mapping['control.title'].nil? || row[@mapping['control.title']].nil?
86
- control['desc'] = row[@mapping['control.desc']] unless @mapping['control.desc'].nil? || row[@mapping['control.desc']].nil?
72
+ control['id'] = generate_control_id(prefix, row[@mapping['control.id']]) unless @mapping['control.id'].nil? || row[@mapping['control.id']].nil?
73
+ control['title'] = row[@mapping['control.title']] unless @mapping['control.title'].nil? || row[@mapping['control.title']].nil?
74
+ control['desc'] = row[@mapping['control.desc']] unless @mapping['control.desc'].nil? || row[@mapping['control.desc']].nil?
87
75
  control['tags'] = {}
88
- nist, nist_rev = get_nist_reference(row[@mapping['control.tags']['cci']]) unless @mapping['control.tags']['cci'].nil? || row[@mapping['control.tags']['cci']].nil?
89
- control['tags']['nist'] = [nist, 'Rev_' + nist_rev] unless nist.nil? || nist_rev.nil?
76
+ cci_number = get_cci_number(row[@mapping['control.tags']['cci']])
77
+ nist = get_nist_reference(cci_number) unless cci_number.nil?
78
+ control['tags']['nist'] = nist unless nist.nil? || nist.include?(nil)
90
79
  @mapping['control.tags'].each do |tag|
91
- control['tags'][tag.first.to_s] = row[tag.last] unless row[tag.last].nil?
80
+ if tag.first == 'cci'
81
+ control['tags'][tag.first] = cci_number
82
+ next
83
+ end
84
+ control['tags'][tag.first] = row[tag.last] unless row[tag.last].nil?
92
85
  end
93
86
  unless @mapping['control.tags']['severity'].nil? || row[@mapping['control.tags']['severity']].nil?
94
87
  control['impact'] = Utils::InspecUtil.get_impact(row[@mapping['control.tags']['severity']])
@@ -97,5 +90,15 @@ module InspecTools
97
90
  @controls << control
98
91
  end
99
92
  end
93
+
94
+ def generate_control_id(prefix, id)
95
+ return id if prefix.nil?
96
+
97
+ "#{prefix}-#{id}"
98
+ end
100
99
  end
101
100
  end
101
+
102
+ # rubocop:enable Metrics/AbcSize
103
+ # rubocop:enable Metrics/PerceivedComplexity
104
+ # rubocop:enable Metrics/CyclomaticComplexity
@@ -0,0 +1,35 @@
1
+ module InspecTools
2
+ class GenerateMap
3
+ attr_accessor :text
4
+
5
+ def initialize(text = nil)
6
+ @text = text.nil? ? default_text : text
7
+ end
8
+
9
+ def generate_example(file)
10
+ File.write(file, @text)
11
+ end
12
+
13
+ private
14
+
15
+ def default_text
16
+ <<~YML
17
+ # Setting csv_header to true will skip the csv file header
18
+ skip_csv_header: true
19
+ width : 80
20
+
21
+
22
+ control.id: 0
23
+ control.title: 15
24
+ control.desc: 16
25
+ control.tags:
26
+ severity: 1
27
+ rid: 8
28
+ stig_id: 3
29
+ cci: 2
30
+ check: 12
31
+ fix: 10
32
+ YML
33
+ end
34
+ end
35
+ end
@@ -9,17 +9,14 @@ require_relative '../happy_mapper_tools/stig_checklist'
9
9
  require_relative '../happy_mapper_tools/benchmark'
10
10
  require_relative '../utilities/inspec_util'
11
11
  require_relative 'csv'
12
-
13
- # rubocop:disable Metrics/ClassLength
14
- # rubocop:disable Metrics/AbcSize
15
- # rubocop:disable Metrics/BlockLength
16
- # rubocop:disable Style/GuardClause
12
+ require_relative '../utilities/xccdf/from_inspec'
13
+ require_relative '../utilities/xccdf/to_xccdf'
17
14
 
18
15
  module InspecTools
19
- class Inspec
20
- def initialize(inspec_json, metadata = '{}')
16
+ class Inspec # rubocop:disable Metrics/ClassLength
17
+ def initialize(inspec_json, metadata = {})
21
18
  @json = JSON.parse(inspec_json.gsub(/\\+u0000/, ''))
22
- @metadata = JSON.parse(metadata)
19
+ @metadata = metadata
23
20
  end
24
21
 
25
22
  def to_ckl(title = nil, date = nil, cklist = nil)
@@ -36,16 +33,15 @@ module InspecTools
36
33
  @checklist.to_xml.encode('UTF-8').gsub('<?xml version="1.0"?>', '<?xml version="1.0" encoding="UTF-8"?>').chomp
37
34
  end
38
35
 
36
+ # Convert Inspec result data to XCCDF
37
+ #
38
+ # @param attributes [Hash] Optional input attributes
39
+ # @return [String] XML formatted String
39
40
  def to_xccdf(attributes, verbose = false)
40
- @data = Utils::InspecUtil.parse_data_for_xccdf(@json)
41
- @attribute = attributes
42
- @attribute = {} if @attribute.eql? false
41
+ data = Utils::FromInspec.new.parse_data_for_xccdf(@json)
43
42
  @verbose = verbose
44
- @benchmark = HappyMapperTools::Benchmark::Benchmark.new
45
- populate_header
46
- # populate_profiles @todo populate profiles; not implemented now because its use is deprecated
47
- populate_groups
48
- @benchmark.to_xml
43
+
44
+ Utils::ToXCCDF.new(attributes || {}, data).to_xml(@metadata)
49
45
  end
50
46
 
51
47
  ####
@@ -70,7 +66,7 @@ module InspecTools
70
66
  #
71
67
  # @param inspec_json : an inspec profile formatted as a json object
72
68
  ###
73
- def inspec_json_to_array(inspec_json)
69
+ def inspec_json_to_array(inspec_json) # rubocop:disable Metrics/CyclomaticComplexity
74
70
  data = []
75
71
  headers = {}
76
72
  inspec_json['controls'].each do |control|
@@ -97,10 +93,11 @@ module InspecTools
97
93
  @data['controls'] << control
98
94
  end
99
95
  end
100
- if json['profiles'].nil?
101
- json['controls'].each do |control|
102
- @data['controls'] << control
103
- end
96
+
97
+ return unless json['profiles'].nil?
98
+
99
+ json['controls'].each do |control|
100
+ @data['controls'] << control
104
101
  end
105
102
  end
106
103
 
@@ -161,7 +158,7 @@ module InspecTools
161
158
  vuln
162
159
  end
163
160
 
164
- def generate_asset
161
+ def generate_asset # rubocop:disable Metrics/AbcSize
165
162
  asset = HappyMapperTools::StigChecklist::Asset.new
166
163
  asset.role = !@metadata['role'].nil? ? @metadata['role'] : 'Workstation'
167
164
  asset.type = !@metadata['type'].nil? ? @metadata['type'] : 'Computing'
@@ -223,75 +220,6 @@ module InspecTools
223
220
  ip
224
221
  end
225
222
 
226
- def populate_header
227
- @benchmark.title = @attribute['benchmark.title']
228
- @benchmark.id = @attribute['benchmark.id']
229
- @benchmark.description = @attribute['benchmark.description']
230
- @benchmark.version = @attribute['benchmark.version']
231
-
232
- @benchmark.status = HappyMapperTools::Benchmark::Status.new
233
- @benchmark.status.status = @attribute['benchmark.status']
234
- @benchmark.status.date = @attribute['benchmark.status.date']
235
-
236
- @benchmark.notice = HappyMapperTools::Benchmark::Notice.new
237
- @benchmark.notice.id = @attribute['benchmark.notice.id']
238
-
239
- @benchmark.plaintext = HappyMapperTools::Benchmark::Plaintext.new
240
- @benchmark.plaintext.plaintext = @attribute['benchmark.plaintext']
241
- @benchmark.plaintext.id = @attribute['benchmark.plaintext.id']
242
-
243
- @benchmark.reference = HappyMapperTools::Benchmark::ReferenceBenchmark.new
244
- @benchmark.reference.href = @attribute['reference.href']
245
- @benchmark.reference.dc_publisher = @attribute['reference.dc.publisher']
246
- @benchmark.reference.dc_source = @attribute['reference.dc.source']
247
- end
248
-
249
- def populate_groups
250
- group_array = []
251
- @data['controls'].each do |control|
252
- group = HappyMapperTools::Benchmark::Group.new
253
- group.id = control['id']
254
- group.title = control['gtitle']
255
- group.description = "<GroupDescription>#{control['gdescription']}</GroupDescription>"
256
-
257
- group.rule = HappyMapperTools::Benchmark::Rule.new
258
- group.rule.id = control['rid']
259
- group.rule.severity = control['severity']
260
- group.rule.weight = control['rweight']
261
- group.rule.version = control['rversion']
262
- group.rule.title = control['title'].tr("\n", ' ')
263
- group.rule.description = "<VulnDiscussion>#{control['desc'].tr("\n", ' ')}</VulnDiscussion><FalsePositives></FalsePositives><FalseNegatives></FalseNegatives><Documentable>false</Documentable><Mitigations></Mitigations><SeverityOverrideGuidance></SeverityOverrideGuidance><PotentialImpacts></PotentialImpacts><ThirdPartyTools></ThirdPartyTools><MitigationControl></MitigationControl><Responsibility></Responsibility><IAControls></IAControls>"
264
-
265
- group.rule.reference = HappyMapperTools::Benchmark::ReferenceGroup.new
266
- group.rule.reference.dc_publisher = @attribute['reference.dc.publisher']
267
- group.rule.reference.dc_title = @attribute['reference.dc.title']
268
- group.rule.reference.dc_subject = @attribute['reference.dc.subject']
269
- group.rule.reference.dc_type = @attribute['reference.dc.type']
270
- group.rule.reference.dc_identifier = @attribute['reference.dc.identifier']
271
-
272
- group.rule.ident = HappyMapperTools::Benchmark::Ident.new
273
- group.rule.ident.system = 'https://public.cyber.mil/stigs/cci/'
274
- group.rule.ident.ident = control['cci']
275
-
276
- group.rule.fixtext = HappyMapperTools::Benchmark::Fixtext.new
277
- group.rule.fixtext.fixref = control['fixref']
278
- group.rule.fixtext.fixtext = control['fix']
279
-
280
- group.rule.fix = HappyMapperTools::Benchmark::Fix.new
281
- group.rule.fix.id = control['fixref']
282
-
283
- group.rule.check = HappyMapperTools::Benchmark::Check.new
284
- group.rule.check.system = control['checkref']
285
- group.rule.check.content_ref = HappyMapperTools::Benchmark::ContentRef.new
286
- group.rule.check.content_ref.name = @attribute['content_ref.name']
287
- group.rule.check.content_ref.href = @attribute['content_ref.href']
288
- group.rule.check.content = control['check']
289
-
290
- group_array << group
291
- end
292
- @benchmark.group = group_array
293
- end
294
-
295
223
  def generate_title(title, json, date)
296
224
  title ||= "Untitled - Checklist Created from Automated InSpec Results JSON; Profiles: #{json['profiles'].map { |x| x['name'] }.join(' | ')}"
297
225
  title + " Checklist Date: #{date || Date.today.to_s}"
@@ -2,9 +2,9 @@ require 'digest'
2
2
 
3
3
  require_relative '../utilities/inspec_util'
4
4
  require_relative '../utilities/extract_pdf_text'
5
- require_relative '../utilities/extract_nist_cis_mapping'
6
5
  require_relative '../utilities/parser'
7
6
  require_relative '../utilities/text_cleaner'
7
+ require_relative '../utilities/cis_to_nist'
8
8
 
9
9
  # rubocop:disable Metrics/AbcSize
10
10
  # rubocop:disable Metrics/PerceivedComplexity
@@ -24,7 +24,7 @@ module InspecTools
24
24
  @controls = []
25
25
  @csv_handle = nil
26
26
  @cci_xml = nil
27
- @nist_mapping = nil
27
+ @nist_mapping = Utils::CisToNist.get_mapping('cis_to_nist_critical_controls')
28
28
  @pdf_text = ''
29
29
  @clean_text = ''
30
30
  @transformed_data = ''
@@ -33,7 +33,6 @@ module InspecTools
33
33
  @title ||= extract_title
34
34
  clean_pdf_text
35
35
  transform_data
36
- read_excl
37
36
  insert_json_metadata
38
37
  @profile['controls'] = parse_controls
39
38
  @profile['sha256'] = Digest::SHA256.hexdigest @profile.to_s
@@ -122,15 +121,5 @@ module InspecTools
122
121
  def write_clean_text
123
122
  File.write('debug_text', @clean_text)
124
123
  end
125
-
126
- def read_excl
127
- nist_map_path = File.join(File.dirname(__FILE__), '../data/NIST_Map_09212017B_CSC-CIS_Critical_Security_Controls_VER_6.1_Excel_9.1.2016.xlsx')
128
- excel = Util::ExtractNistMappings.new(nist_map_path)
129
- @nist_mapping = excel.full_excl
130
- rescue StandardError => e
131
- puts "Exception: #{e.message}"
132
- puts 'Existing...'
133
- exit
134
- end
135
124
  end
136
125
  end
@@ -1,4 +1,3 @@
1
- require 'yaml'
2
1
  require 'json'
3
2
  require 'roo'
4
3
  require_relative '../utilities/inspec_util'
@@ -13,8 +12,8 @@ module InspecTools
13
12
  autoload :CKL, 'inspec_tools/ckl'
14
13
  autoload :Inspec, 'inspec_tools/inspec'
15
14
  autoload :Summary, 'inspec_tools/summary'
16
- autoload :Threshold, 'inspec_tools/threshold'
17
15
  autoload :XLSXTool, 'inspec_tools/xlsx_tool'
16
+ autoload :GenerateMap, 'inspec_tools/generate_map'
18
17
  end
19
18
 
20
19
  # rubocop:disable Style/GuardClause
@@ -23,7 +22,7 @@ module InspecPlugins
23
22
  class CliCommand < Inspec.plugin(2, :cli_command) # rubocop:disable Metrics/ClassLength
24
23
  POSSIBLE_LOG_LEVELS = %w{debug info warn error fatal}.freeze
25
24
 
26
- class_option :log_directory, type: :string, aliases: :l, desc: 'Provie log location'
25
+ class_option :log_directory, type: :string, aliases: :l, desc: 'Provide log location'
27
26
  class_option :log_level, type: :string, desc: "Set the logging level: #{POSSIBLE_LOG_LEVELS}"
28
27
 
29
28
  subcommand_desc 'tools [COMMAND]', 'Runs inspec_tools commands through Inspec'
@@ -54,12 +53,18 @@ module InspecPlugins
54
53
 
55
54
  desc 'inspec2xccdf', 'inspec2xccdf translates an inspec profile and attributes files to an xccdf file'
56
55
  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'
56
+ option :inspec_json, required: true, aliases: '-j',
57
+ desc: 'path to InSpec JSON file created'
58
+ option :attributes, required: true, aliases: '-a',
59
+ desc: 'path to yml file that provides the required attributes for the XCCDF document. These attributes are parts of XCCDF document which do not fit into the InSpec schema.'
60
+ option :output, required: true, aliases: '-o',
61
+ desc: 'name or path to create the XCCDF and title to give the XCCDF'
62
+ option :metadata, required: false, type: :string, aliases: '-m',
63
+ desc: 'path to JSON file with additional host metadata for the XCCDF file'
60
64
  def inspec2xccdf
61
65
  json = File.read(options[:inspec_json])
62
- inspec_tool = InspecTools::Inspec.new(json)
66
+ metadata = options[:metadata] ? JSON.parse(File.read(options[:metadata])) : {}
67
+ inspec_tool = InspecTools::Inspec.new(json, metadata)
63
68
  attr_hsh = YAML.load_file(options[:attributes])
64
69
  xccdf = inspec_tool.to_xccdf(attr_hsh)
65
70
  File.write(options[:output], xccdf)
@@ -73,10 +78,11 @@ module InspecPlugins
73
78
  option :output, required: false, aliases: '-o', default: 'profile'
74
79
  option :format, required: false, aliases: '-f', enum: %w{ruby hash}, default: 'ruby'
75
80
  option :separate_files, required: false, type: :boolean, default: true, aliases: '-s'
81
+ option :control_name_prefix, required: false, type: :string, aliases: '-p'
76
82
  def csv2inspec
77
83
  csv = CSV.read(options[:csv], encoding: 'ISO8859-1')
78
84
  mapping = YAML.load_file(options[:mapping])
79
- profile = InspecTools::CSVTool.new(csv, mapping, options[:csv].split('/')[-1].split('.')[0], options[:verbose]).to_inspec
85
+ profile = InspecTools::CSVTool.new(csv, mapping, options[:csv].split('/')[-1].split('.')[0], options[:verbose]).to_inspec(control_name_prefix: options[:control_name_prefix])
80
86
  Utils::InspecUtil.unpack_inspec_json(options[:output], profile, options[:separate_files], options[:format])
81
87
  end
82
88
 
@@ -136,26 +142,8 @@ module InspecPlugins
136
142
 
137
143
  desc 'generate_map', 'Generates mapping template from CSV to Inspec Controls'
138
144
  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
145
+ generator = InspecTools::GenerateMap.new
146
+ generator.generate_example('mapping.yml')
159
147
  end
160
148
 
161
149
  desc 'generate_ckl_metadata', 'Generate metadata file that can be passed to inspec2ckl'
@@ -200,26 +188,14 @@ module InspecPlugins
200
188
  desc 'summary', 'summary parses an inspec results json to create a summary json'
201
189
  long_desc InspecTools::Help.text(:summary)
202
190
  option :inspec_json, required: true, aliases: '-j'
203
- option :verbose, type: :boolean, aliases: '-V'
204
191
  option :json_full, type: :boolean, required: false, aliases: '-f'
205
192
  option :json_counts, type: :boolean, required: false, aliases: '-k'
193
+ option :threshold_file, required: false, aliases: '-t'
194
+ option :threshold_inline, required: false, aliases: '-i'
206
195
 
207
196
  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]
197
+ summary = InspecTools::Summary.new(options: options)
198
+ summary.output_summary
223
199
  end
224
200
 
225
201
  desc 'compliance', 'compliance parses an inspec results json to check if the compliance level meets a specified threshold'
@@ -227,17 +203,10 @@ module InspecPlugins
227
203
  option :inspec_json, required: true, aliases: '-j'
228
204
  option :threshold_file, required: false, aliases: '-f'
229
205
  option :threshold_inline, required: false, aliases: '-i'
230
- option :verbose, type: :boolean, aliases: '-V'
231
206
 
232
207
  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)
208
+ compliance = InspecTools::Summary.new(options: options)
209
+ compliance.results_meet_threshold? ? exit(0) : exit(1)
241
210
  end
242
211
  end
243
212
  end