inspec_tools 2.0.7 → 2.3.3
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 +20 -12
- data/Rakefile +9 -1
- data/lib/happy_mapper_tools/benchmark.rb +83 -0
- data/lib/happy_mapper_tools/stig_attributes.rb +10 -1
- 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/plugin_cli.rb +22 -53
- data/lib/inspec_tools/summary.rb +108 -76
- data/lib/inspec_tools/xccdf.rb +12 -3
- data/lib/inspec_tools/xlsx_tool.rb +2 -1
- data/lib/utilities/cci_xml.rb +13 -0
- data/lib/utilities/inspec_util.rb +12 -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 +42 -36
@@ -1,4 +1,3 @@
|
|
1
|
-
require 'yaml'
|
2
1
|
require 'json'
|
3
2
|
require 'roo'
|
4
3
|
require_relative '../utilities/inspec_util'
|
@@ -13,8 +12,8 @@ module InspecTools
|
|
13
12
|
autoload :CKL, 'inspec_tools/ckl'
|
14
13
|
autoload :Inspec, 'inspec_tools/inspec'
|
15
14
|
autoload :Summary, 'inspec_tools/summary'
|
16
|
-
autoload :Threshold, 'inspec_tools/threshold'
|
17
15
|
autoload :XLSXTool, 'inspec_tools/xlsx_tool'
|
16
|
+
autoload :GenerateMap, 'inspec_tools/generate_map'
|
18
17
|
end
|
19
18
|
|
20
19
|
# rubocop:disable Style/GuardClause
|
@@ -23,7 +22,7 @@ module InspecPlugins
|
|
23
22
|
class CliCommand < Inspec.plugin(2, :cli_command) # rubocop:disable Metrics/ClassLength
|
24
23
|
POSSIBLE_LOG_LEVELS = %w{debug info warn error fatal}.freeze
|
25
24
|
|
26
|
-
class_option :log_directory, type: :string, aliases: :l, desc: '
|
25
|
+
class_option :log_directory, type: :string, aliases: :l, desc: 'Provide log location'
|
27
26
|
class_option :log_level, type: :string, desc: "Set the logging level: #{POSSIBLE_LOG_LEVELS}"
|
28
27
|
|
29
28
|
subcommand_desc 'tools [COMMAND]', 'Runs inspec_tools commands through Inspec'
|
@@ -54,12 +53,18 @@ module InspecPlugins
|
|
54
53
|
|
55
54
|
desc 'inspec2xccdf', 'inspec2xccdf translates an inspec profile and attributes files to an xccdf file'
|
56
55
|
long_desc InspecTools::Help.text(:inspec2xccdf)
|
57
|
-
option :inspec_json, required: true, aliases: '-j'
|
58
|
-
|
59
|
-
option :
|
56
|
+
option :inspec_json, required: true, aliases: '-j',
|
57
|
+
desc: 'path to InSpec JSON file created'
|
58
|
+
option :attributes, required: true, aliases: '-a',
|
59
|
+
desc: 'path to yml file that provides the required attributes for the XCCDF document. These attributes are parts of XCCDF document which do not fit into the InSpec schema.'
|
60
|
+
option :output, required: true, aliases: '-o',
|
61
|
+
desc: 'name or path to create the XCCDF and title to give the XCCDF'
|
62
|
+
option :metadata, required: false, type: :string, aliases: '-m',
|
63
|
+
desc: 'path to JSON file with additional host metadata for the XCCDF file'
|
60
64
|
def inspec2xccdf
|
61
65
|
json = File.read(options[:inspec_json])
|
62
|
-
|
66
|
+
metadata = options[:metadata] ? JSON.parse(File.read(options[:metadata])) : {}
|
67
|
+
inspec_tool = InspecTools::Inspec.new(json, metadata)
|
63
68
|
attr_hsh = YAML.load_file(options[:attributes])
|
64
69
|
xccdf = inspec_tool.to_xccdf(attr_hsh)
|
65
70
|
File.write(options[:output], xccdf)
|
@@ -73,10 +78,11 @@ module InspecPlugins
|
|
73
78
|
option :output, required: false, aliases: '-o', default: 'profile'
|
74
79
|
option :format, required: false, aliases: '-f', enum: %w{ruby hash}, default: 'ruby'
|
75
80
|
option :separate_files, required: false, type: :boolean, default: true, aliases: '-s'
|
81
|
+
option :control_name_prefix, required: false, type: :string, aliases: '-p'
|
76
82
|
def csv2inspec
|
77
83
|
csv = CSV.read(options[:csv], encoding: 'ISO8859-1')
|
78
84
|
mapping = YAML.load_file(options[:mapping])
|
79
|
-
profile = InspecTools::CSVTool.new(csv, mapping, options[:csv].split('/')[-1].split('.')[0], options[:verbose]).to_inspec
|
85
|
+
profile = InspecTools::CSVTool.new(csv, mapping, options[:csv].split('/')[-1].split('.')[0], options[:verbose]).to_inspec(control_name_prefix: options[:control_name_prefix])
|
80
86
|
Utils::InspecUtil.unpack_inspec_json(options[:output], profile, options[:separate_files], options[:format])
|
81
87
|
end
|
82
88
|
|
@@ -136,26 +142,8 @@ module InspecPlugins
|
|
136
142
|
|
137
143
|
desc 'generate_map', 'Generates mapping template from CSV to Inspec Controls'
|
138
144
|
def generate_map
|
139
|
-
|
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
|
145
|
+
generator = InspecTools::GenerateMap.new
|
146
|
+
generator.generate_example('mapping.yml')
|
159
147
|
end
|
160
148
|
|
161
149
|
desc 'generate_ckl_metadata', 'Generate metadata file that can be passed to inspec2ckl'
|
@@ -200,26 +188,14 @@ module InspecPlugins
|
|
200
188
|
desc 'summary', 'summary parses an inspec results json to create a summary json'
|
201
189
|
long_desc InspecTools::Help.text(:summary)
|
202
190
|
option :inspec_json, required: true, aliases: '-j'
|
203
|
-
option :verbose, type: :boolean, aliases: '-V'
|
204
191
|
option :json_full, type: :boolean, required: false, aliases: '-f'
|
205
192
|
option :json_counts, type: :boolean, required: false, aliases: '-k'
|
193
|
+
option :threshold_file, required: false, aliases: '-t'
|
194
|
+
option :threshold_inline, required: false, aliases: '-i'
|
206
195
|
|
207
196
|
def summary
|
208
|
-
summary = InspecTools::Summary.new(
|
209
|
-
|
210
|
-
unless options.include?('json_full') || options.include?('json_counts')
|
211
|
-
puts "\nOverall compliance: #{summary[:compliance]}%\n\n"
|
212
|
-
summary[:status].keys.each do |category|
|
213
|
-
puts category
|
214
|
-
summary[:status][category].keys.each do |impact|
|
215
|
-
puts "\t#{impact} : #{summary[:status][category][impact]}"
|
216
|
-
end
|
217
|
-
end
|
218
|
-
end
|
219
|
-
|
220
|
-
json_summary = summary.to_json
|
221
|
-
puts json_summary if options[:json_full]
|
222
|
-
puts summary[:status].to_json if options[:json_counts]
|
197
|
+
summary = InspecTools::Summary.new(options: options)
|
198
|
+
summary.output_summary
|
223
199
|
end
|
224
200
|
|
225
201
|
desc 'compliance', 'compliance parses an inspec results json to check if the compliance level meets a specified threshold'
|
@@ -227,17 +203,10 @@ module InspecPlugins
|
|
227
203
|
option :inspec_json, required: true, aliases: '-j'
|
228
204
|
option :threshold_file, required: false, aliases: '-f'
|
229
205
|
option :threshold_inline, required: false, aliases: '-i'
|
230
|
-
option :verbose, type: :boolean, aliases: '-V'
|
231
206
|
|
232
207
|
def compliance
|
233
|
-
|
234
|
-
|
235
|
-
exit(1)
|
236
|
-
end
|
237
|
-
threshold = YAML.load_file(options[:threshold_file]) unless options[:threshold_file].nil?
|
238
|
-
threshold = YAML.safe_load(options[:threshold_inline]) unless options[:threshold_inline].nil?
|
239
|
-
compliance = InspecTools::Summary.new(File.read(options[:inspec_json])).threshold(threshold)
|
240
|
-
compliance ? exit(0) : exit(1)
|
208
|
+
compliance = InspecTools::Summary.new(options: options)
|
209
|
+
compliance.results_meet_threshold? ? exit(0) : exit(1)
|
241
210
|
end
|
242
211
|
end
|
243
212
|
end
|
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
@@ -17,7 +17,7 @@ module InspecTools
|
|
17
17
|
@xccdf = replace_tags_in_xccdf(replace_tags, @xccdf) unless replace_tags.nil?
|
18
18
|
cci_list_path = File.join(File.dirname(__FILE__), '../data/U_CCI_List.xml')
|
19
19
|
@cci_items = HappyMapperTools::CCIAttributes::CCI_List.parse(File.read(cci_list_path))
|
20
|
-
|
20
|
+
register_after_parse_callbacks
|
21
21
|
@benchmark = HappyMapperTools::StigAttributes::Benchmark.parse(@xccdf)
|
22
22
|
end
|
23
23
|
|
@@ -89,6 +89,14 @@ module InspecTools
|
|
89
89
|
|
90
90
|
private
|
91
91
|
|
92
|
+
def register_after_parse_callbacks
|
93
|
+
# Determine if the parsed Ident is refrencing a legacy ID number.
|
94
|
+
HappyMapperTools::StigAttributes::Ident.after_parse do |object|
|
95
|
+
object.cci = object.system.eql?('http://cyber.mil/cci')
|
96
|
+
object.legacy = !object.cci
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
92
100
|
def replace_tags_in_xccdf(replace_tags, xccdf_xml)
|
93
101
|
replace_tags.each do |tag|
|
94
102
|
xccdf_xml = xccdf_xml.gsub(/(<|<)#{tag}(>|>)/, "$#{tag}")
|
@@ -133,8 +141,9 @@ module InspecTools
|
|
133
141
|
control['tags']['rid'] = group.rule.id
|
134
142
|
control['tags']['stig_id'] = group.rule.version
|
135
143
|
control['tags']['fix_id'] = group.rule.fix.id
|
136
|
-
control['tags']['cci'] = group.rule.idents
|
137
|
-
control['tags']['
|
144
|
+
control['tags']['cci'] = group.rule.idents.select { |i| i.cci }.map { |i| i.ident }
|
145
|
+
control['tags']['legacy'] = group.rule.idents.select { |i| i.legacy}.map { |i| i.ident }
|
146
|
+
control['tags']['nist'] = @cci_items.fetch_nists(control['tags']['cci'])
|
138
147
|
control['tags']['false_negatives'] = group.rule.description.false_negatives if group.rule.description.false_negatives != ''
|
139
148
|
control['tags']['false_positives'] = group.rule.description.false_positives if group.rule.description.false_positives != ''
|
140
149
|
control['tags']['documentable'] = group.rule.description.documentable if group.rule.description.documentable != ''
|
@@ -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
|
@@ -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
|
@@ -305,6 +246,7 @@ module Utils
|
|
305
246
|
control.add_tag(::Inspec::Object::Tag.new('stig_id', json_control['tags']['stig_id']))
|
306
247
|
control.add_tag(::Inspec::Object::Tag.new('fix_id', json_control['tags']['fix_id']))
|
307
248
|
control.add_tag(::Inspec::Object::Tag.new('cci', json_control['tags']['cci']))
|
249
|
+
control.add_tag(::Inspec::Object::Tag.new('legacy', json_control['tags']['legacy']))
|
308
250
|
control.add_tag(::Inspec::Object::Tag.new('nist', json_control['tags']['nist']))
|
309
251
|
control.add_tag(::Inspec::Object::Tag.new('cis_level', json_control['tags']['cis_level'])) unless json_control['tags']['cis_level'].blank?
|
310
252
|
control.add_tag(::Inspec::Object::Tag.new('cis_controls', json_control['tags']['cis_controls'])) unless json_control['tags']['cis_controls'].blank?
|
@@ -384,7 +326,7 @@ module Utils
|
|
384
326
|
myfile.puts readme_contents
|
385
327
|
end
|
386
328
|
|
387
|
-
private_class_method def self.unpack_profile(directory, controls, separated, output_format)
|
329
|
+
private_class_method def self.unpack_profile(directory, controls, separated, output_format) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
388
330
|
FileUtils.rm_rf(directory) if Dir.exist?(directory)
|
389
331
|
Dir.mkdir directory unless Dir.exist?(directory)
|
390
332
|
Dir.mkdir "#{directory}/controls" unless Dir.exist?("#{directory}/controls")
|
@@ -433,9 +375,3 @@ module Utils
|
|
433
375
|
end
|
434
376
|
end
|
435
377
|
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
|