inspec_tools 2.0.4 → 2.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +25 -17
- 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/happy_mapper_tools/stig_attributes.rb +68 -48
- 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/xccdf.rb +1 -0
- 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 +10 -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 +48 -29
- data/CHANGELOG.md +0 -714
- 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
data/lib/inspec_tools/summary.rb
CHANGED
@@ -2,8 +2,6 @@ require 'json'
|
|
2
2
|
require 'yaml'
|
3
3
|
require_relative '../utilities/inspec_util'
|
4
4
|
|
5
|
-
# rubocop:disable Metrics/AbcSize
|
6
|
-
|
7
5
|
# Impact Definitions
|
8
6
|
CRITICAL = 0.9
|
9
7
|
HIGH = 0.7
|
@@ -16,48 +14,118 @@ TALLYS = %i(total critical high medium low).freeze
|
|
16
14
|
THRESHOLD_TEMPLATE = File.expand_path('../data/threshold.yaml', File.dirname(__FILE__))
|
17
15
|
|
18
16
|
module InspecTools
|
17
|
+
# rubocop:disable Metrics/ClassLength
|
19
18
|
class Summary
|
20
|
-
|
21
|
-
|
19
|
+
attr_reader :json
|
20
|
+
attr_reader :json_full
|
21
|
+
attr_reader :json_counts
|
22
|
+
attr_reader :threshold_file
|
23
|
+
attr_reader :threshold_inline
|
24
|
+
attr_reader :summary
|
25
|
+
attr_reader :threshold
|
26
|
+
|
27
|
+
def initialize(**options)
|
28
|
+
options = options[:options]
|
29
|
+
@json = JSON.parse(File.read(options[:inspec_json]))
|
30
|
+
@json_full = false || options[:json_full]
|
31
|
+
@json_counts = false || options[:json_counts]
|
32
|
+
@threshold = parse_threshold(options[:threshold_inline], options[:threshold_file])
|
33
|
+
@threshold_provided = options[:threshold_inline] || options[:threshold_file]
|
34
|
+
@summary = compute_summary
|
22
35
|
end
|
23
36
|
|
24
|
-
def
|
25
|
-
@
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
37
|
+
def output_summary
|
38
|
+
unless @json_full || @json_counts
|
39
|
+
puts "\nThreshold compliance: #{@threshold['compliance.min']}%"
|
40
|
+
puts "\nOverall compliance: #{@summary[:compliance]}%\n\n"
|
41
|
+
@summary[:status].keys.each do |category|
|
42
|
+
puts category
|
43
|
+
@summary[:status][category].keys.each do |impact|
|
44
|
+
puts "\t#{impact} : #{@summary[:status][category][impact]}"
|
45
|
+
end
|
46
|
+
end
|
31
47
|
end
|
32
|
-
|
33
|
-
@summary
|
48
|
+
|
49
|
+
puts @summary.to_json if @json_full
|
50
|
+
puts @summary[:status].to_json if @json_counts
|
34
51
|
end
|
35
52
|
|
36
|
-
def
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
53
|
+
def results_meet_threshold?
|
54
|
+
raise 'Please provide threshold as a yaml file or inline yaml' unless @threshold_provided
|
55
|
+
|
56
|
+
compliance = true
|
57
|
+
failure = []
|
58
|
+
failure << check_max_compliance(@threshold['compliance.max'], @summary[:compliance], '', 'compliance')
|
59
|
+
failure << check_min_compliance(@threshold['compliance.min'], @summary[:compliance], '', 'compliance')
|
60
|
+
|
61
|
+
BUCKETS.each do |bucket|
|
62
|
+
TALLYS.each do |tally|
|
63
|
+
failure << check_min_compliance(@threshold["#{bucket}.#{tally}.min"], @summary[:status][bucket][tally], bucket, tally)
|
64
|
+
failure << check_max_compliance(@threshold["#{bucket}.#{tally}.max"], @summary[:status][bucket][tally], bucket, tally)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
failure.reject!(&:nil?)
|
69
|
+
compliance = false if failure.length.positive?
|
70
|
+
output(compliance, failure)
|
71
|
+
compliance
|
41
72
|
end
|
42
73
|
|
43
74
|
private
|
44
75
|
|
76
|
+
def check_min_compliance(min, data, bucket, tally)
|
77
|
+
expected_to_string(bucket, tally, 'min', min, data) if min != -1 and data < min
|
78
|
+
end
|
79
|
+
|
80
|
+
def check_max_compliance(max, data, bucket, tally)
|
81
|
+
expected_to_string(bucket, tally, 'max', max, data) if max != -1 and data > max
|
82
|
+
end
|
83
|
+
|
84
|
+
def output(passed_threshold, what_failed)
|
85
|
+
if passed_threshold
|
86
|
+
puts "Overall compliance threshold of #{@threshold['compliance.min']}\% met. Current compliance at #{@summary[:compliance]}\%"
|
87
|
+
else
|
88
|
+
puts 'Compliance threshold was not met: '
|
89
|
+
puts what_failed.join("\n")
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def expected_to_string(bucket, tally, maxmin, value, got)
|
94
|
+
return "Expected #{bucket}.#{tally}.#{maxmin}:#{value} got:#{got}" unless bucket.empty? || bucket.nil?
|
95
|
+
|
96
|
+
"Expected #{tally}.#{maxmin}:#{value}\% got:#{got}\%"
|
97
|
+
end
|
98
|
+
|
99
|
+
def parse_threshold(threshold_inline, threshold_file)
|
100
|
+
threshold = Utils::InspecUtil.to_dotted_hash(YAML.load_file(THRESHOLD_TEMPLATE))
|
101
|
+
threshold.merge!(Utils::InspecUtil.to_dotted_hash(YAML.load_file(threshold_file))) if threshold_file
|
102
|
+
threshold.merge!(Utils::InspecUtil.to_dotted_hash(YAML.safe_load(threshold_inline))) if threshold_inline
|
103
|
+
threshold
|
104
|
+
end
|
105
|
+
|
45
106
|
def compute_summary
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
107
|
+
data = Utils::InspecUtil.parse_data_for_ckl(@json)
|
108
|
+
|
109
|
+
data.keys.each do |control_id|
|
110
|
+
current_control = data[control_id]
|
111
|
+
current_control[:compliance_status] = Utils::InspecUtil.control_status(current_control, true)
|
112
|
+
current_control[:finding_details] = Utils::InspecUtil.control_finding_details(current_control, current_control[:compliance_status])
|
113
|
+
end
|
114
|
+
|
115
|
+
summary = {}
|
116
|
+
summary[:buckets] = {}
|
117
|
+
summary[:buckets][:failed] = select_by_status(data, 'Open')
|
118
|
+
summary[:buckets][:passed] = select_by_status(data, 'NotAFinding')
|
119
|
+
summary[:buckets][:no_impact] = select_by_status(data, 'Not_Applicable')
|
120
|
+
summary[:buckets][:skipped] = select_by_status(data, 'Not_Reviewed')
|
121
|
+
summary[:buckets][:error] = select_by_status(data, 'Profile_Error')
|
122
|
+
|
123
|
+
summary[:status] = {}
|
124
|
+
%i(failed passed no_impact skipped error).each do |key|
|
125
|
+
summary[:status][key] = tally_by_impact(summary[:buckets][key])
|
126
|
+
end
|
127
|
+
summary[:compliance] = compute_compliance(summary)
|
128
|
+
summary
|
61
129
|
end
|
62
130
|
|
63
131
|
def select_by_impact(controls, impact)
|
@@ -78,49 +146,13 @@ module InspecTools
|
|
78
146
|
tally
|
79
147
|
end
|
80
148
|
|
81
|
-
def compute_compliance
|
82
|
-
(
|
83
|
-
(
|
84
|
-
|
85
|
-
|
86
|
-
|
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][tally] > max
|
108
|
-
compliance = false
|
109
|
-
failure << "Expected #{bucket}.#{tally}.max:#{max} got:#{status[bucket][tally]}"
|
110
|
-
end
|
111
|
-
if min != -1 and status[bucket][tally] < min
|
112
|
-
compliance = false
|
113
|
-
failure << "Expected #{bucket}.#{tally}.min:#{min} got:#{status[bucket][tally]}"
|
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)
|
149
|
+
def compute_compliance(summary)
|
150
|
+
(summary[:status][:passed][:total]*100.0/
|
151
|
+
(summary[:status][:passed][:total]+
|
152
|
+
summary[:status][:failed][:total]+
|
153
|
+
summary[:status][:skipped][:total]+
|
154
|
+
summary[:status][:error][:total])).floor
|
124
155
|
end
|
125
156
|
end
|
157
|
+
# rubocop:enable Metrics/ClassLength
|
126
158
|
end
|
data/lib/inspec_tools/xccdf.rb
CHANGED
@@ -140,6 +140,7 @@ module InspecTools
|
|
140
140
|
control['tags']['documentable'] = group.rule.description.documentable if group.rule.description.documentable != ''
|
141
141
|
control['tags']['mitigations'] = group.rule.description.false_negatives if group.rule.description.mitigations != ''
|
142
142
|
control['tags']['severity_override_guidance'] = group.rule.description.severity_override_guidance if group.rule.description.severity_override_guidance != ''
|
143
|
+
control['tags']['security_override_guidance'] = group.rule.description.security_override_guidance if group.rule.description.security_override_guidance != ''
|
143
144
|
control['tags']['potential_impacts'] = group.rule.description.potential_impacts if group.rule.description.potential_impacts != ''
|
144
145
|
control['tags']['third_party_tools'] = group.rule.description.third_party_tools if group.rule.description.third_party_tools != ''
|
145
146
|
control['tags']['mitigation_controls'] = group.rule.description.mitigation_controls if group.rule.description.mitigation_controls != ''
|
@@ -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
|
@@ -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
|
@@ -315,6 +256,7 @@ module Utils
|
|
315
256
|
control.add_tag(::Inspec::Object::Tag.new('documentable', json_control['tags']['documentable'])) unless json_control['tags']['documentable'].blank?
|
316
257
|
control.add_tag(::Inspec::Object::Tag.new('mitigations', json_control['tags']['mitigations'])) unless json_control['tags']['mitigations'].blank?
|
317
258
|
control.add_tag(::Inspec::Object::Tag.new('severity_override_guidance', json_control['tags']['severity_override_guidance'])) unless json_control['tags']['severity_override_guidance'].blank?
|
259
|
+
control.add_tag(::Inspec::Object::Tag.new('security_override_guidance', json_control['tags']['security_override_guidance'])) unless json_control['tags']['security_override_guidance'].blank?
|
318
260
|
control.add_tag(::Inspec::Object::Tag.new('potential_impacts', json_control['tags']['potential_impacts'])) unless json_control['tags']['potential_impacts'].blank?
|
319
261
|
control.add_tag(::Inspec::Object::Tag.new('third_party_tools', json_control['tags']['third_party_tools'])) unless json_control['tags']['third_party_tools'].blank?
|
320
262
|
control.add_tag(::Inspec::Object::Tag.new('mitigation_controls', json_control['tags']['mitigation_controls'])) unless json_control['tags']['mitigation_controls'].blank?
|
@@ -383,7 +325,7 @@ module Utils
|
|
383
325
|
myfile.puts readme_contents
|
384
326
|
end
|
385
327
|
|
386
|
-
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
|
387
329
|
FileUtils.rm_rf(directory) if Dir.exist?(directory)
|
388
330
|
Dir.mkdir directory unless Dir.exist?(directory)
|
389
331
|
Dir.mkdir "#{directory}/controls" unless Dir.exist?("#{directory}/controls")
|
@@ -432,9 +374,3 @@ module Utils
|
|
432
374
|
end
|
433
375
|
end
|
434
376
|
end
|
435
|
-
|
436
|
-
# rubocop:enable Metrics/ClassLength
|
437
|
-
# rubocop:enable Metrics/AbcSize
|
438
|
-
# rubocop:enable Metrics/PerceivedComplexity
|
439
|
-
# rubocop:enable Metrics/CyclomaticComplexity
|
440
|
-
# 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
|