inspec_tools 2.0.7 → 2.1.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.
- checksums.yaml +4 -4
- data/README.md +7 -5
- data/Rakefile +9 -1
- data/lib/happy_mapper_tools/benchmark.rb +83 -0
- data/lib/inspec_tools/csv.rb +26 -33
- data/lib/inspec_tools/generate_map.rb +35 -0
- data/lib/inspec_tools/inspec.rb +19 -91
- data/lib/inspec_tools/plugin_cli.rb +14 -25
- data/lib/inspec_tools/xlsx_tool.rb +2 -1
- data/lib/utilities/cci_xml.rb +13 -0
- data/lib/utilities/inspec_util.rb +9 -74
- 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 -25
@@ -15,6 +15,7 @@ module InspecTools
|
|
15
15
|
autoload :Summary, 'inspec_tools/summary'
|
16
16
|
autoload :Threshold, 'inspec_tools/threshold'
|
17
17
|
autoload :XLSXTool, 'inspec_tools/xlsx_tool'
|
18
|
+
autoload :GenerateMap, 'inspec_tools/generate_map'
|
18
19
|
end
|
19
20
|
|
20
21
|
# rubocop:disable Style/GuardClause
|
@@ -23,7 +24,7 @@ module InspecPlugins
|
|
23
24
|
class CliCommand < Inspec.plugin(2, :cli_command) # rubocop:disable Metrics/ClassLength
|
24
25
|
POSSIBLE_LOG_LEVELS = %w{debug info warn error fatal}.freeze
|
25
26
|
|
26
|
-
class_option :log_directory, type: :string, aliases: :l, desc: '
|
27
|
+
class_option :log_directory, type: :string, aliases: :l, desc: 'Provide log location'
|
27
28
|
class_option :log_level, type: :string, desc: "Set the logging level: #{POSSIBLE_LOG_LEVELS}"
|
28
29
|
|
29
30
|
subcommand_desc 'tools [COMMAND]', 'Runs inspec_tools commands through Inspec'
|
@@ -54,12 +55,18 @@ module InspecPlugins
|
|
54
55
|
|
55
56
|
desc 'inspec2xccdf', 'inspec2xccdf translates an inspec profile and attributes files to an xccdf file'
|
56
57
|
long_desc InspecTools::Help.text(:inspec2xccdf)
|
57
|
-
option :inspec_json, required: true, aliases: '-j'
|
58
|
-
|
59
|
-
option :
|
58
|
+
option :inspec_json, required: true, aliases: '-j',
|
59
|
+
desc: 'path to InSpec JSON file created'
|
60
|
+
option :attributes, required: true, aliases: '-a',
|
61
|
+
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.'
|
62
|
+
option :output, required: true, aliases: '-o',
|
63
|
+
desc: 'name or path to create the XCCDF and title to give the XCCDF'
|
64
|
+
option :metadata, required: false, type: :string, aliases: '-m',
|
65
|
+
desc: 'path to JSON file with additional host metadata for the XCCDF file'
|
60
66
|
def inspec2xccdf
|
61
67
|
json = File.read(options[:inspec_json])
|
62
|
-
|
68
|
+
metadata = options[:metadata] ? JSON.parse(File.read(options[:metadata])) : {}
|
69
|
+
inspec_tool = InspecTools::Inspec.new(json, metadata)
|
63
70
|
attr_hsh = YAML.load_file(options[:attributes])
|
64
71
|
xccdf = inspec_tool.to_xccdf(attr_hsh)
|
65
72
|
File.write(options[:output], xccdf)
|
@@ -136,26 +143,8 @@ module InspecPlugins
|
|
136
143
|
|
137
144
|
desc 'generate_map', 'Generates mapping template from CSV to Inspec Controls'
|
138
145
|
def generate_map
|
139
|
-
|
140
|
-
|
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
|
146
|
+
generator = InspecTools::GenerateMap.new
|
147
|
+
generator.generate_example('mapping.yml')
|
159
148
|
end
|
160
149
|
|
161
150
|
desc 'generate_ckl_metadata', 'Generate metadata file that can be passed to inspec2ckl'
|
@@ -6,6 +6,7 @@ require 'digest'
|
|
6
6
|
|
7
7
|
require_relative '../utilities/inspec_util'
|
8
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
|
@@ -19,7 +20,7 @@ module InspecTools
|
|
19
20
|
def initialize(xlsx, mapping, name, verbose = false)
|
20
21
|
@name = name
|
21
22
|
@xlsx = xlsx
|
22
|
-
@mapping = mapping
|
23
|
+
@mapping = Utils::MappingValidator.validate(mapping)
|
23
24
|
@verbose = verbose
|
24
25
|
@cis_to_nist = Utils::CisToNist.get_mapping('cis_to_nist_mapping')
|
25
26
|
end
|
@@ -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
|
@@ -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
|
@@ -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
|