inspec_tools 2.0.6 → 2.3.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +21 -13
- data/Rakefile +82 -8
- data/lib/data/cis_to_nist_critical_controls +0 -0
- data/lib/data/cis_to_nist_mapping +0 -0
- data/lib/happy_mapper_tools/benchmark.rb +83 -0
- data/lib/inspec_tools/csv.rb +42 -39
- data/lib/inspec_tools/generate_map.rb +35 -0
- data/lib/inspec_tools/inspec.rb +19 -91
- data/lib/inspec_tools/pdf.rb +2 -13
- data/lib/inspec_tools/plugin_cli.rb +22 -53
- data/lib/inspec_tools/summary.rb +108 -76
- data/lib/inspec_tools/xlsx_tool.rb +4 -16
- data/lib/utilities/cci_xml.rb +13 -0
- data/lib/utilities/cis_to_nist.rb +11 -0
- data/lib/utilities/inspec_util.rb +11 -76
- data/lib/utilities/mapping_validator.rb +10 -0
- data/lib/utilities/xccdf/from_inspec.rb +89 -0
- data/lib/utilities/xccdf/to_xccdf.rb +388 -0
- data/lib/utilities/xccdf/xccdf_score.rb +116 -0
- metadata +45 -40
- data/CHANGELOG.md +0 -736
- data/lib/data/NIST_Map_02052020_CIS_Controls_Version_7.1_Implementation_Groups_1.2.xlsx +0 -0
- data/lib/data/NIST_Map_09212017B_CSC-CIS_Critical_Security_Controls_VER_6.1_Excel_9.1.2016.xlsx +0 -0
- data/lib/utilities/extract_nist_cis_mapping.rb +0 -57
@@ -3,9 +3,10 @@ require 'inspec-objects'
|
|
3
3
|
require 'word_wrap'
|
4
4
|
require 'yaml'
|
5
5
|
require 'digest'
|
6
|
-
require 'roo'
|
7
6
|
|
8
7
|
require_relative '../utilities/inspec_util'
|
8
|
+
require_relative '../utilities/cis_to_nist'
|
9
|
+
require_relative '../utilities/mapping_validator'
|
9
10
|
|
10
11
|
# rubocop:disable Metrics/AbcSize
|
11
12
|
# rubocop:disable Metrics/PerceivedComplexity
|
@@ -14,15 +15,14 @@ require_relative '../utilities/inspec_util'
|
|
14
15
|
module InspecTools
|
15
16
|
# Methods for converting from XLS to various formats
|
16
17
|
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
18
|
LATEST_NIST_REV = 'Rev_4'.freeze
|
19
19
|
|
20
20
|
def initialize(xlsx, mapping, name, verbose = false)
|
21
21
|
@name = name
|
22
22
|
@xlsx = xlsx
|
23
|
-
@mapping = mapping
|
23
|
+
@mapping = Utils::MappingValidator.validate(mapping)
|
24
24
|
@verbose = verbose
|
25
|
-
@cis_to_nist =
|
25
|
+
@cis_to_nist = Utils::CisToNist.get_mapping('cis_to_nist_mapping')
|
26
26
|
end
|
27
27
|
|
28
28
|
def to_ckl
|
@@ -46,18 +46,6 @@ module InspecTools
|
|
46
46
|
|
47
47
|
private
|
48
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
49
|
def insert_json_metadata
|
62
50
|
@profile['name'] = @name
|
63
51
|
@profile['title'] = 'InSpec Profile'
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
|
3
|
+
module Utils
|
4
|
+
class CciXml
|
5
|
+
def self.get_cci_list(cci_list_file)
|
6
|
+
path = File.expand_path(File.join(File.expand_path(__dir__), '..', 'data', cci_list_file))
|
7
|
+
raise "CCI list does not exist at #{path}" unless File.exist?(path)
|
8
|
+
|
9
|
+
cci_list = Nokogiri::XML(File.open(path))
|
10
|
+
cci_list.remove_namespaces!
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Utils
|
2
|
+
class CisToNist
|
3
|
+
def self.get_mapping(mapping_file)
|
4
|
+
path = File.expand_path(File.join(File.expand_path(__dir__), '..', 'data', mapping_file))
|
5
|
+
raise "CIS to NIST control mapping does not exist at #{path}. Has it been generated?" unless File.exist?(path)
|
6
|
+
|
7
|
+
mapping = File.open(path)
|
8
|
+
Marshal.load(mapping)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -13,15 +13,8 @@ require 'overrides/object'
|
|
13
13
|
require 'overrides/string'
|
14
14
|
require 'rubocop'
|
15
15
|
|
16
|
-
# rubocop:disable Metrics/ClassLength
|
17
|
-
# rubocop:disable Metrics/AbcSize
|
18
|
-
# rubocop:disable Metrics/PerceivedComplexity
|
19
|
-
# rubocop:disable Metrics/CyclomaticComplexity
|
20
|
-
# rubocop:disable Metrics/MethodLength
|
21
|
-
|
22
16
|
module Utils
|
23
|
-
class InspecUtil
|
24
|
-
DATA_NOT_FOUND_MESSAGE = 'N/A'.freeze
|
17
|
+
class InspecUtil # rubocop:disable Metrics/ClassLength
|
25
18
|
WIDTH = 80
|
26
19
|
IMPACT_SCORES = {
|
27
20
|
'none' => 0.0,
|
@@ -31,56 +24,7 @@ module Utils
|
|
31
24
|
'critical' => 0.9
|
32
25
|
}.freeze
|
33
26
|
|
34
|
-
def self.
|
35
|
-
data = {}
|
36
|
-
|
37
|
-
controls = []
|
38
|
-
if json['profiles'].nil?
|
39
|
-
controls = json['controls']
|
40
|
-
elsif json['profiles'].length == 1
|
41
|
-
controls = json['profiles'].last['controls']
|
42
|
-
else
|
43
|
-
json['profiles'].each do |profile|
|
44
|
-
controls.concat(profile['controls'])
|
45
|
-
end
|
46
|
-
end
|
47
|
-
c_data = {}
|
48
|
-
|
49
|
-
controls.each do |control|
|
50
|
-
c_id = control['id'].to_sym
|
51
|
-
c_data[c_id] = {}
|
52
|
-
c_data[c_id]['id'] = control['id'] || DATA_NOT_FOUND_MESSAGE
|
53
|
-
c_data[c_id]['title'] = control['title'] || DATA_NOT_FOUND_MESSAGE
|
54
|
-
c_data[c_id]['desc'] = control['desc'] || DATA_NOT_FOUND_MESSAGE
|
55
|
-
c_data[c_id]['severity'] = control['tags']['severity'] || DATA_NOT_FOUND_MESSAGE
|
56
|
-
c_data[c_id]['gid'] = control['tags']['gid'] || DATA_NOT_FOUND_MESSAGE
|
57
|
-
c_data[c_id]['gtitle'] = control['tags']['gtitle'] || DATA_NOT_FOUND_MESSAGE
|
58
|
-
c_data[c_id]['gdescription'] = control['tags']['gdescription'] || DATA_NOT_FOUND_MESSAGE
|
59
|
-
c_data[c_id]['rid'] = control['tags']['rid'] || DATA_NOT_FOUND_MESSAGE
|
60
|
-
c_data[c_id]['rversion'] = control['tags']['rversion'] || DATA_NOT_FOUND_MESSAGE
|
61
|
-
c_data[c_id]['rweight'] = control['tags']['rweight'] || DATA_NOT_FOUND_MESSAGE
|
62
|
-
c_data[c_id]['stig_id'] = control['tags']['stig_id'] || DATA_NOT_FOUND_MESSAGE
|
63
|
-
c_data[c_id]['cci'] = control['tags']['cci'] || DATA_NOT_FOUND_MESSAGE
|
64
|
-
c_data[c_id]['nist'] = control['tags']['nist'] || ['unmapped']
|
65
|
-
c_data[c_id]['check'] = control['tags']['check'] || DATA_NOT_FOUND_MESSAGE
|
66
|
-
c_data[c_id]['checkref'] = control['tags']['checkref'] || DATA_NOT_FOUND_MESSAGE
|
67
|
-
c_data[c_id]['fix'] = control['tags']['fix'] || DATA_NOT_FOUND_MESSAGE
|
68
|
-
c_data[c_id]['fixref'] = control['tags']['fixref'] || DATA_NOT_FOUND_MESSAGE
|
69
|
-
c_data[c_id]['fix_id'] = control['tags']['fix_id'] || DATA_NOT_FOUND_MESSAGE
|
70
|
-
c_data[c_id]['rationale'] = control['tags']['rationale'] || DATA_NOT_FOUND_MESSAGE
|
71
|
-
c_data[c_id]['cis_family'] = control['tags']['cis_family'] || DATA_NOT_FOUND_MESSAGE
|
72
|
-
c_data[c_id]['cis_rid'] = control['tags']['cis_rid'] || DATA_NOT_FOUND_MESSAGE
|
73
|
-
c_data[c_id]['cis_level'] = control['tags']['cis_level'] || DATA_NOT_FOUND_MESSAGE
|
74
|
-
c_data[c_id]['impact'] = control['impact'].to_s || DATA_NOT_FOUND_MESSAGE
|
75
|
-
c_data[c_id]['code'] = control['code'].to_s || DATA_NOT_FOUND_MESSAGE
|
76
|
-
end
|
77
|
-
|
78
|
-
data['controls'] = c_data.values
|
79
|
-
data['status'] = 'success'
|
80
|
-
data
|
81
|
-
end
|
82
|
-
|
83
|
-
def self.parse_data_for_ckl(json)
|
27
|
+
def self.parse_data_for_ckl(json) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
84
28
|
data = {}
|
85
29
|
|
86
30
|
# Parse for inspec profile results json
|
@@ -104,8 +48,8 @@ module Utils
|
|
104
48
|
end
|
105
49
|
|
106
50
|
if control['descriptions'].respond_to?(:find)
|
107
|
-
data[c_id][:check_content] = control['descriptions'].find { |c| c['label'] == '
|
108
|
-
data[c_id][:fix_text] = control['descriptions'].find { |c| c['label'] == '
|
51
|
+
data[c_id][:check_content] = control['descriptions'].find { |c| c['label'] == 'check' }&.dig('data')
|
52
|
+
data[c_id][:fix_text] = control['descriptions'].find { |c| c['label'] == 'fix' }&.dig('data')
|
109
53
|
end
|
110
54
|
|
111
55
|
data[c_id][:impact] = control['impact'].to_s unless control['impact'].nil?
|
@@ -221,7 +165,7 @@ module Utils
|
|
221
165
|
end
|
222
166
|
|
223
167
|
private_class_method def self.string_to_impact(severity, use_cvss_terms)
|
224
|
-
if
|
168
|
+
if %r{none|na|n/a|not[_|(\s*)]?applicable}i.match?(severity)
|
225
169
|
impact = 0.0 # Informative
|
226
170
|
elsif /low|cat(egory)?\s*(iii|3)/i.match?(severity)
|
227
171
|
impact = 0.3 # Low Impact
|
@@ -248,13 +192,10 @@ module Utils
|
|
248
192
|
end
|
249
193
|
|
250
194
|
IMPACT_SCORES.reverse_each do |name, impact_score|
|
251
|
-
if name == 'critical' && value >= impact_score && use_cvss_terms
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
else
|
256
|
-
next
|
257
|
-
end
|
195
|
+
return 'high' if name == 'critical' && value >= impact_score && use_cvss_terms
|
196
|
+
return name if value >= impact_score
|
197
|
+
|
198
|
+
next
|
258
199
|
end
|
259
200
|
end
|
260
201
|
|
@@ -277,7 +218,7 @@ module Utils
|
|
277
218
|
WordWrap.ww(str.to_s, width)
|
278
219
|
end
|
279
220
|
|
280
|
-
private_class_method def self.generate_controls(inspec_json)
|
221
|
+
private_class_method def self.generate_controls(inspec_json) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
281
222
|
controls = []
|
282
223
|
inspec_json['controls'].each do |json_control|
|
283
224
|
control = ::Inspec::Object::Control.new
|
@@ -384,7 +325,7 @@ module Utils
|
|
384
325
|
myfile.puts readme_contents
|
385
326
|
end
|
386
327
|
|
387
|
-
private_class_method def self.unpack_profile(directory, controls, separated, output_format)
|
328
|
+
private_class_method def self.unpack_profile(directory, controls, separated, output_format) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
388
329
|
FileUtils.rm_rf(directory) if Dir.exist?(directory)
|
389
330
|
Dir.mkdir directory unless Dir.exist?(directory)
|
390
331
|
Dir.mkdir "#{directory}/controls" unless Dir.exist?("#{directory}/controls")
|
@@ -433,9 +374,3 @@ module Utils
|
|
433
374
|
end
|
434
375
|
end
|
435
376
|
end
|
436
|
-
|
437
|
-
# rubocop:enable Metrics/ClassLength
|
438
|
-
# rubocop:enable Metrics/AbcSize
|
439
|
-
# rubocop:enable Metrics/PerceivedComplexity
|
440
|
-
# rubocop:enable Metrics/CyclomaticComplexity
|
441
|
-
# rubocop:enable Metrics/MethodLength
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module Utils
|
2
|
+
class MappingValidator
|
3
|
+
def self.validate(mapping)
|
4
|
+
return mapping unless mapping.include?('control.tags') && mapping['control.tags'].include?('nist')
|
5
|
+
|
6
|
+
raise "Mapping file should not contain an entry for 'control.tags.nist'.
|
7
|
+
NIST tags will be autogenerated via 'control.tags.cci'."
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module Utils
|
2
|
+
# Data transformation from Inspec result output into usable data for XCCDF conversions.
|
3
|
+
class FromInspec
|
4
|
+
DATA_NOT_FOUND_MESSAGE = 'N/A'.freeze
|
5
|
+
|
6
|
+
# Convert raw Inspec result json into format acceptable for XCCDF transformation.
|
7
|
+
def parse_data_for_xccdf(json) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
8
|
+
data = {}
|
9
|
+
|
10
|
+
controls = []
|
11
|
+
if json['profiles'].nil?
|
12
|
+
controls = json['controls']
|
13
|
+
elsif json['profiles'].length == 1
|
14
|
+
controls = json['profiles'].last['controls']
|
15
|
+
else
|
16
|
+
json['profiles'].each do |profile|
|
17
|
+
controls.concat(profile['controls'])
|
18
|
+
end
|
19
|
+
end
|
20
|
+
c_data = {}
|
21
|
+
|
22
|
+
controls.each do |control|
|
23
|
+
c_id = control['id'].to_sym
|
24
|
+
c_data[c_id] = {}
|
25
|
+
c_data[c_id]['id'] = control['id'] || DATA_NOT_FOUND_MESSAGE
|
26
|
+
c_data[c_id]['title'] = control['title'] if control['title'] # Optional attribute
|
27
|
+
c_data[c_id]['desc'] = control['desc'] || DATA_NOT_FOUND_MESSAGE
|
28
|
+
c_data[c_id]['severity'] = control['tags']['severity'] || 'unknown'
|
29
|
+
c_data[c_id]['gid'] = control['tags']['gid'] || control['id']
|
30
|
+
c_data[c_id]['gtitle'] = control['tags']['gtitle'] if control['tags']['gtitle'] # Optional attribute
|
31
|
+
c_data[c_id]['gdescription'] = control['tags']['gdescription'] if control['tags']['gdescription'] # Optional attribute
|
32
|
+
c_data[c_id]['rid'] = control['tags']['rid'] || 'r_' + c_data[c_id]['gid']
|
33
|
+
c_data[c_id]['rversion'] = control['tags']['rversion'] if control['tags']['rversion'] # Optional attribute
|
34
|
+
c_data[c_id]['rweight'] = control['tags']['rweight'] if control['tags']['rweight'] # Optional attribute where N/A is not schema compliant
|
35
|
+
c_data[c_id]['stig_id'] = control['tags']['stig_id'] || DATA_NOT_FOUND_MESSAGE
|
36
|
+
c_data[c_id]['cci'] = control['tags']['cci'] if control['tags']['cci'] # Optional attribute
|
37
|
+
c_data[c_id]['nist'] = control['tags']['nist'] || ['unmapped']
|
38
|
+
c_data[c_id]['check'] = control['tags']['check'] || DATA_NOT_FOUND_MESSAGE
|
39
|
+
c_data[c_id]['checkref'] = control['tags']['checkref'] || DATA_NOT_FOUND_MESSAGE
|
40
|
+
c_data[c_id]['fix'] = control['tags']['fix'] || DATA_NOT_FOUND_MESSAGE
|
41
|
+
c_data[c_id]['fix_id'] = control['tags']['fix_id'] if control['tags']['fix_id'] # Optional attribute where N/A is not schema compliant
|
42
|
+
c_data[c_id]['rationale'] = control['tags']['rationale'] || DATA_NOT_FOUND_MESSAGE
|
43
|
+
c_data[c_id]['cis_family'] = control['tags']['cis_family'] || DATA_NOT_FOUND_MESSAGE
|
44
|
+
c_data[c_id]['cis_rid'] = control['tags']['cis_rid'] || DATA_NOT_FOUND_MESSAGE
|
45
|
+
c_data[c_id]['cis_level'] = control['tags']['cis_level'] || DATA_NOT_FOUND_MESSAGE
|
46
|
+
c_data[c_id]['impact'] = control['impact'].to_s || DATA_NOT_FOUND_MESSAGE
|
47
|
+
c_data[c_id]['code'] = control['code'].to_s || DATA_NOT_FOUND_MESSAGE
|
48
|
+
c_data[c_id]['results'] = parse_results_for_xccdf(control['results']) if control['results']
|
49
|
+
end
|
50
|
+
|
51
|
+
data['controls'] = c_data.values
|
52
|
+
data['profiles'] = parse_profiles_for_xccdf(json['profiles'])
|
53
|
+
data['status'] = 'success'
|
54
|
+
data['inspec_version'] = json['version']
|
55
|
+
data
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
# Convert profile information for result processing
|
61
|
+
# @param profiles [Array[Hash]] - The profiles section of the JSON output
|
62
|
+
def parse_profiles_for_xccdf(profiles)
|
63
|
+
return [] unless profiles
|
64
|
+
|
65
|
+
profiles.map do |profile|
|
66
|
+
data = {}
|
67
|
+
data['name'] = profile['name']
|
68
|
+
data['version'] = profile['version']
|
69
|
+
data
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Convert the test result data to a parseable Hash for downstream processing
|
74
|
+
# @param results [Array[Hash]] - The results section of the JSON output
|
75
|
+
def parse_results_for_xccdf(results)
|
76
|
+
results.map do |result|
|
77
|
+
data = {}
|
78
|
+
data['status'] = result['status']
|
79
|
+
data['code_desc'] = result['code_desc']
|
80
|
+
data['run_time'] = result['run_time']
|
81
|
+
data['start_time'] = result['start_time']
|
82
|
+
data['resource'] = result['resource']
|
83
|
+
data['message'] = result['message']
|
84
|
+
data['skip_message'] = result['skip_message']
|
85
|
+
data
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,388 @@
|
|
1
|
+
require_relative 'xccdf_score'
|
2
|
+
|
3
|
+
module Utils
|
4
|
+
# Data conversions for Inspec output into XCCDF format.
|
5
|
+
class ToXCCDF # rubocop:disable Metrics/ClassLength
|
6
|
+
# @param attribute [Hash] XCCDF supplemental attributes
|
7
|
+
# @param data [Hash] Converted Inspec output data
|
8
|
+
def initialize(attribute, data)
|
9
|
+
@attribute = attribute
|
10
|
+
@data = data
|
11
|
+
@benchmark = HappyMapperTools::Benchmark::Benchmark.new
|
12
|
+
end
|
13
|
+
|
14
|
+
# Build entire XML document and produce final output
|
15
|
+
# @param metadata [Hash] Data representing a system under scan
|
16
|
+
def to_xml(metadata)
|
17
|
+
build_benchmark_header
|
18
|
+
build_groups
|
19
|
+
# Only populate results if a target is defined so that conformant XML is produced.
|
20
|
+
@benchmark.testresult = build_test_results(metadata) if metadata['fqdn']
|
21
|
+
@benchmark.to_xml
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
# Sets top level XCCDF Benchmark attributes
|
27
|
+
def build_benchmark_header
|
28
|
+
@benchmark.title = @attribute['benchmark.title']
|
29
|
+
@benchmark.id = @attribute['benchmark.id']
|
30
|
+
@benchmark.description = @attribute['benchmark.description']
|
31
|
+
@benchmark.version = @attribute['benchmark.version']
|
32
|
+
@benchmark.xmlns = 'http://checklists.nist.gov/xccdf/1.1'
|
33
|
+
|
34
|
+
@benchmark.status = HappyMapperTools::Benchmark::Status.new
|
35
|
+
@benchmark.status.status = @attribute['benchmark.status']
|
36
|
+
@benchmark.status.date = @attribute['benchmark.status.date']
|
37
|
+
|
38
|
+
if @attribute['benchmark.notice.id']
|
39
|
+
@benchmark.notice = HappyMapperTools::Benchmark::Notice.new
|
40
|
+
@benchmark.notice.id = @attribute['benchmark.notice.id']
|
41
|
+
end
|
42
|
+
|
43
|
+
if @attribute['benchmark.plaintext'] || @attribute['benchmark.plaintext.id']
|
44
|
+
@benchmark.plaintext = HappyMapperTools::Benchmark::Plaintext.new
|
45
|
+
@benchmark.plaintext.plaintext = @attribute['benchmark.plaintext']
|
46
|
+
@benchmark.plaintext.id = @attribute['benchmark.plaintext.id']
|
47
|
+
end
|
48
|
+
|
49
|
+
@benchmark.reference = HappyMapperTools::Benchmark::ReferenceBenchmark.new
|
50
|
+
@benchmark.reference.href = @attribute['reference.href']
|
51
|
+
@benchmark.reference.dc_publisher = @attribute['reference.dc.publisher']
|
52
|
+
@benchmark.reference.dc_source = @attribute['reference.dc.source']
|
53
|
+
end
|
54
|
+
|
55
|
+
# Translate join of Inspec results and input attributes to XCCDF Groups
|
56
|
+
def build_groups # rubocop:disable Metrics/AbcSize
|
57
|
+
group_array = []
|
58
|
+
@data['controls'].each do |control|
|
59
|
+
group = HappyMapperTools::Benchmark::Group.new
|
60
|
+
group.id = control['id']
|
61
|
+
group.title = control['gtitle']
|
62
|
+
group.description = "<GroupDescription>#{control['gdescription']}</GroupDescription>" if control['gdescription']
|
63
|
+
|
64
|
+
group.rule = HappyMapperTools::Benchmark::Rule.new
|
65
|
+
group.rule.id = control['rid']
|
66
|
+
group.rule.severity = control['severity']
|
67
|
+
group.rule.weight = control['rweight']
|
68
|
+
group.rule.version = control['rversion']
|
69
|
+
group.rule.title = control['title'].tr("\n", ' ') if control['title']
|
70
|
+
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>"
|
71
|
+
|
72
|
+
if ['reference.dc.publisher', 'reference.dc.title', 'reference.dc.subject', 'reference.dc.type', 'reference.dc.identifier'].any? { |a| @attribute.key?(a) }
|
73
|
+
group.rule.reference = build_rule_reference
|
74
|
+
end
|
75
|
+
|
76
|
+
group.rule.ident = build_rule_idents(control['cci']) if control['cci']
|
77
|
+
|
78
|
+
group.rule.fixtext = HappyMapperTools::Benchmark::Fixtext.new
|
79
|
+
group.rule.fixtext.fixref = control['fix_id']
|
80
|
+
group.rule.fixtext.fixtext = control['fix']
|
81
|
+
|
82
|
+
group.rule.fix = build_rule_fix(control['fix_id']) if control['fix_id']
|
83
|
+
|
84
|
+
group.rule.check = HappyMapperTools::Benchmark::Check.new
|
85
|
+
group.rule.check.system = control['checkref']
|
86
|
+
|
87
|
+
# content_ref is optional for schema compliance
|
88
|
+
if @attribute['content_ref.name'] || @attribute['content_ref.href']
|
89
|
+
group.rule.check.content_ref = HappyMapperTools::Benchmark::ContentRef.new
|
90
|
+
group.rule.check.content_ref.name = @attribute['content_ref.name']
|
91
|
+
group.rule.check.content_ref.href = @attribute['content_ref.href']
|
92
|
+
end
|
93
|
+
|
94
|
+
group.rule.check.content = control['check']
|
95
|
+
|
96
|
+
group_array << group
|
97
|
+
end
|
98
|
+
@benchmark.group = group_array
|
99
|
+
end
|
100
|
+
|
101
|
+
# Construct a Benchmark Testresult from Inspec data. This must be called after all XML processing has occurred for profiles
|
102
|
+
# and groups.
|
103
|
+
# @param metadata [Hash]
|
104
|
+
# @return [TestResult]
|
105
|
+
def build_test_results(metadata)
|
106
|
+
test_result = HappyMapperTools::Benchmark::TestResult.new
|
107
|
+
test_result.version = @benchmark.version
|
108
|
+
populate_remark(test_result)
|
109
|
+
populate_target_facts(test_result, metadata)
|
110
|
+
populate_identity(test_result, metadata)
|
111
|
+
populate_results(test_result)
|
112
|
+
populate_score(test_result, @benchmark.group)
|
113
|
+
|
114
|
+
test_result
|
115
|
+
end
|
116
|
+
|
117
|
+
# Contruct a Rule / RuleResult fix element with the provided id.
|
118
|
+
def build_rule_fix(fix_id)
|
119
|
+
HappyMapperTools::Benchmark::Fix.new.tap { |f| f.id = fix_id }
|
120
|
+
end
|
121
|
+
|
122
|
+
# Construct rule identifiers for rule
|
123
|
+
# @param idents [Array]
|
124
|
+
def build_rule_idents(idents)
|
125
|
+
raise "#{idents} is not an Array type." unless idents.is_a?(Array)
|
126
|
+
|
127
|
+
# Each rule identifier is a different element
|
128
|
+
idents.map do |identifier|
|
129
|
+
ident = HappyMapperTools::Benchmark::Ident.new
|
130
|
+
ident.system = 'https://public.cyber.mil/stigs/cci/'
|
131
|
+
ident.ident = identifier
|
132
|
+
ident
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# Contruct a Rule reference element
|
137
|
+
def build_rule_reference
|
138
|
+
reference = HappyMapperTools::Benchmark::ReferenceGroup.new
|
139
|
+
reference.dc_publisher = @attribute['reference.dc.publisher']
|
140
|
+
reference.dc_title = @attribute['reference.dc.title']
|
141
|
+
reference.dc_subject = @attribute['reference.dc.subject']
|
142
|
+
reference.dc_type = @attribute['reference.dc.type']
|
143
|
+
reference.dc_identifier = @attribute['reference.dc.identifier']
|
144
|
+
reference
|
145
|
+
end
|
146
|
+
|
147
|
+
# Create a remark with contextual information about the Inspec version and profiles used
|
148
|
+
# @param result [HappyMapperTools::Benchmark::TestResult]
|
149
|
+
def populate_remark(result)
|
150
|
+
result.remark = "Results created using Inspec version #{@data['inspec_version']}.\n#{@data['profiles'].map { |p| "Profile: #{p['name']} Version: #{p['version']}" }.join("\n")}"
|
151
|
+
end
|
152
|
+
|
153
|
+
# Create all target specific information.
|
154
|
+
# @param result [HappyMapperTools::Benchmark::TestResult]
|
155
|
+
# @param metadata [Hash]
|
156
|
+
def populate_target_facts(result, metadata)
|
157
|
+
result.target = metadata['fqdn']
|
158
|
+
result.target_address = metadata['ip'] if metadata['ip']
|
159
|
+
|
160
|
+
all_facts = []
|
161
|
+
|
162
|
+
if metadata['mac']
|
163
|
+
fact = HappyMapperTools::Benchmark::Fact.new
|
164
|
+
fact.name = 'urn:xccdf:fact:asset:identifier:mac'
|
165
|
+
fact.type = 'string'
|
166
|
+
fact.fact = metadata['mac']
|
167
|
+
all_facts << fact
|
168
|
+
end
|
169
|
+
|
170
|
+
if metadata['ip']
|
171
|
+
fact = HappyMapperTools::Benchmark::Fact.new
|
172
|
+
fact.name = 'urn:xccdf:fact:asset:identifier:ipv4'
|
173
|
+
fact.type = 'string'
|
174
|
+
fact.fact = metadata['ip']
|
175
|
+
all_facts << fact
|
176
|
+
end
|
177
|
+
|
178
|
+
return unless all_facts.size.nonzero?
|
179
|
+
|
180
|
+
facts = HappyMapperTools::Benchmark::TargetFact.new
|
181
|
+
facts.fact = all_facts
|
182
|
+
result.target_facts = facts
|
183
|
+
end
|
184
|
+
|
185
|
+
# Build out the TestResult given all the control and result data.
|
186
|
+
def populate_results(test_result)
|
187
|
+
# Note: id is not an XCCDF 1.2 compliant identifier and will need to be updated when that support is added.
|
188
|
+
test_result.id = 'result_1'
|
189
|
+
test_result.starttime = run_start_time
|
190
|
+
test_result.endtime = run_end_time
|
191
|
+
|
192
|
+
# Build out individual results
|
193
|
+
all_rule_result = []
|
194
|
+
|
195
|
+
@data['controls'].each do |control|
|
196
|
+
next if control['results'].empty?
|
197
|
+
|
198
|
+
control_results =
|
199
|
+
control['results'].map do |result|
|
200
|
+
populate_rule_result(control, result, xccdf_status(result['status'], control['impact']))
|
201
|
+
end
|
202
|
+
|
203
|
+
# Consolidate results into single rule result do to lack of multiple=true attribute on Rule.
|
204
|
+
# 1. Select the unified result status
|
205
|
+
selected_status = control_results.reduce(control_results.first.result) { |f_status, rule_result| xccdf_and_result(f_status, rule_result.result) }
|
206
|
+
|
207
|
+
# 2. Only choose results with that status
|
208
|
+
# 3. Combine those results
|
209
|
+
all_rule_result << combine_results(control_results.select { |r| r.result == selected_status })
|
210
|
+
end
|
211
|
+
|
212
|
+
test_result.rule_result = all_rule_result
|
213
|
+
test_result
|
214
|
+
end
|
215
|
+
|
216
|
+
# Create rule-result from the control and Inspec result information
|
217
|
+
def populate_rule_result(control, result, result_status)
|
218
|
+
rule_result = HappyMapperTools::Benchmark::RuleResultType.new
|
219
|
+
|
220
|
+
rule_result.idref = control['rid']
|
221
|
+
rule_result.severity = control['severity']
|
222
|
+
rule_result.time = end_time(result['start_time'], result['run_time'])
|
223
|
+
rule_result.weight = control['rweight']
|
224
|
+
|
225
|
+
rule_result.result = result_status
|
226
|
+
rule_result.message = result_message(result, result_status) if result_message(result, result_status)
|
227
|
+
rule_result.instance = result['code_desc']
|
228
|
+
|
229
|
+
rule_result.ident = build_rule_idents(control['cci']) if control['cci']
|
230
|
+
|
231
|
+
# Fix information is only necessary when there are failed tests
|
232
|
+
rule_result.fix = build_rule_fix(control['fix_id']) if control['fix_id'] && result_status == 'fail'
|
233
|
+
|
234
|
+
rule_result.check = HappyMapperTools::Benchmark::Check.new
|
235
|
+
rule_result.check.system = control['checkref']
|
236
|
+
rule_result.check.content = result['code_desc']
|
237
|
+
rule_result
|
238
|
+
end
|
239
|
+
|
240
|
+
# Combines rule results with the same result into a single rule result.
|
241
|
+
def combine_results(rule_results) # rubocop:disable Metrics/AbcSize
|
242
|
+
return rule_results.first if rule_results.size == 1
|
243
|
+
|
244
|
+
# Can combine, result, idents (duplicate, take first instance), instance - combine into an array removing duplicates
|
245
|
+
# check.content - Only one value allowed, combine by joining with line feed. Prior to, make sure all values are unique.
|
246
|
+
|
247
|
+
rule_result = HappyMapperTools::Benchmark::RuleResultType.new
|
248
|
+
rule_result.idref = rule_results.first.idref
|
249
|
+
rule_result.severity = rule_results.first.severity
|
250
|
+
# Take latest time
|
251
|
+
rule_result.time = rule_results.reduce(rule_results.first.time) { |time, r| time > r.time ? time : r.time }
|
252
|
+
rule_result.weight = rule_results.first.weight
|
253
|
+
|
254
|
+
rule_result.result = rule_results.first.result
|
255
|
+
rule_result.message = rule_results.reduce([]) { |messages, r| r.message ? messages.push(r.message) : messages }
|
256
|
+
rule_result.instance = rule_results.reduce([]) { |instances, r| r.instance ? instances.push(r.instance) : instances }.join("\n")
|
257
|
+
|
258
|
+
rule_result.ident = rule_results.first.ident
|
259
|
+
rule_result.fix = rule_results.first.fix
|
260
|
+
|
261
|
+
if rule_results.first.check
|
262
|
+
rule_result.check = HappyMapperTools::Benchmark::Check.new
|
263
|
+
rule_result.check.system = rule_results.first.check.system
|
264
|
+
rule_result.check.content = rule_results.map { |r| r.check.content }.join("\n")
|
265
|
+
end
|
266
|
+
|
267
|
+
rule_result
|
268
|
+
end
|
269
|
+
|
270
|
+
# Add information about the the account and organization executing the tests.
|
271
|
+
def populate_identity(test_result, metadata)
|
272
|
+
if metadata['identity']
|
273
|
+
test_result.identity = HappyMapperTools::Benchmark::IdentityType.new
|
274
|
+
test_result.identity.authenticated = true
|
275
|
+
test_result.identity.identity = metadata['identity']['identity']
|
276
|
+
test_result.identity.privileged = metadata['identity']['privileged']
|
277
|
+
end
|
278
|
+
|
279
|
+
test_result.organization = metadata['organization'] if metadata['organization']
|
280
|
+
end
|
281
|
+
|
282
|
+
# Return the earliest time of execution.
|
283
|
+
def run_start_time
|
284
|
+
@data['controls'].map { |control| control['results'].map { |result| DateTime.parse(result['start_time']) } }.flatten.min
|
285
|
+
end
|
286
|
+
|
287
|
+
# Return the latest time of execution accounting for Inspec duration.
|
288
|
+
def run_end_time
|
289
|
+
end_times =
|
290
|
+
@data['controls'].map do |control|
|
291
|
+
control['results'].map { |result| end_time(result['start_time'], result['run_time']) }
|
292
|
+
end
|
293
|
+
|
294
|
+
end_times.flatten.max
|
295
|
+
end
|
296
|
+
|
297
|
+
# Calculate an end time given a start time and second duration
|
298
|
+
def end_time(start, duration)
|
299
|
+
DateTime.parse(start) + (duration / (24*60*60))
|
300
|
+
end
|
301
|
+
|
302
|
+
# Map the Inspec result status to appropriate XCCDF test result status.
|
303
|
+
# XCCDF options include: pass, fail, error, unknown, notapplicable, notchecked, notselected, informational, fixed
|
304
|
+
#
|
305
|
+
# @param inspec_status [String] The reported Inspec status from an individual test
|
306
|
+
# @param impact [String] A value of 0.0 - 1.0
|
307
|
+
# @return A valid Inspec status.
|
308
|
+
def xccdf_status(inspec_status, impact)
|
309
|
+
# Currently, there is no good way to map an Inspec result status to one of XCCDF status unknown or notselected.
|
310
|
+
case inspec_status
|
311
|
+
when 'failed'
|
312
|
+
'fail'
|
313
|
+
when 'passed'
|
314
|
+
'pass'
|
315
|
+
when 'skipped'
|
316
|
+
if impact.to_f.zero?
|
317
|
+
'notapplicable'
|
318
|
+
else
|
319
|
+
'notchecked'
|
320
|
+
end
|
321
|
+
else
|
322
|
+
# In the event Inspec adds a new unaccounted for status, mapping to XCCDF unknown.
|
323
|
+
'unknown'
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
# When more than one result occurs for a rule and the specification does not declare multiple, the result must be combined.
|
328
|
+
# This determines the appropriate result to be selected when there are two to compare.
|
329
|
+
# @param one [String] A rule-result status
|
330
|
+
# @param two [String] A rule-result status
|
331
|
+
# @return The result of the AND operation.
|
332
|
+
def xccdf_and_result(one, two) # rubocop:disable Metrics/CyclomaticComplexity
|
333
|
+
# From XCCDF specification truth table
|
334
|
+
# P = pass
|
335
|
+
# F = fail
|
336
|
+
# U = unknown
|
337
|
+
# E = error
|
338
|
+
# N = notapplicable
|
339
|
+
# K = notchecked
|
340
|
+
# S = notselected
|
341
|
+
# I = informational
|
342
|
+
|
343
|
+
case one
|
344
|
+
when 'pass'
|
345
|
+
%w{fail unknown}.any? { |s| s == two } ? two : one
|
346
|
+
when 'fail'
|
347
|
+
one
|
348
|
+
when 'unknown'
|
349
|
+
two == 'fail' ? two : one
|
350
|
+
when 'notapplicable'
|
351
|
+
%w{pass fail unknown}.any? { |s| s == two } ? two : one
|
352
|
+
when 'notchecked'
|
353
|
+
%w{pass fail unknown notapplicable}.any? { |s| s == two } ? two : one
|
354
|
+
end
|
355
|
+
end
|
356
|
+
|
357
|
+
# Builds the message information for rule results
|
358
|
+
# @param result [Hash] A single Inspec result
|
359
|
+
# @param xccdf_status [String] the xccdf calculated result status for the provided result
|
360
|
+
def result_message(result, xccdf_status)
|
361
|
+
return unless result['message'] || result['skip_message']
|
362
|
+
|
363
|
+
message = HappyMapperTools::Benchmark::MessageType.new
|
364
|
+
# Including the code of the check and the resulting message if there is one.
|
365
|
+
message.message = "#{result['code_desc'] ? result['code_desc'] + "\n\n" : ''}#{result['message'] || result['skip_message']}"
|
366
|
+
message.severity = result_message_severity(xccdf_status)
|
367
|
+
message
|
368
|
+
end
|
369
|
+
|
370
|
+
# All rule-result messages require a defined severity. This determines a value to use based upon the result XCCDF status.
|
371
|
+
def result_message_severity(xccdf_status)
|
372
|
+
case xccdf_status
|
373
|
+
when 'fail'
|
374
|
+
'error'
|
375
|
+
when 'notapplicable'
|
376
|
+
'warning'
|
377
|
+
else
|
378
|
+
'info'
|
379
|
+
end
|
380
|
+
end
|
381
|
+
|
382
|
+
# Set scores for all 4 required/recommended scoring systems.
|
383
|
+
def populate_score(test_result, groups)
|
384
|
+
score = Utils::XCCDFScore.new(groups, test_result.rule_result)
|
385
|
+
test_result.score = [score.default_score, score.flat_score, score.flat_unweighted_score, score.absolute_score]
|
386
|
+
end
|
387
|
+
end
|
388
|
+
end
|