inspec_tools 2.1.0 → 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 +13 -7
- data/lib/inspec_tools/csv.rb +16 -6
- data/lib/inspec_tools/plugin_cli.rb +8 -28
- data/lib/inspec_tools/summary.rb +108 -76
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 315e07faccf97b313b9493963ef7b9b80ffa7a1459bfa661527c4e827de89676
|
4
|
+
data.tar.gz: e31f0bc2c0006bcb96b9ee46f34c59b8ba71358c2293d7c39874b0fc80b5292b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 50409617c4e6142916f328e5850a5ef3f2d99b9e71671714ce0475708c435242911b6d4455f47fd1f5e50a7778b80e673d4ec74e3ee5d8f66ab5530c42fa8ad9
|
7
|
+
data.tar.gz: e34adf2df0ec1b1bcd5d9224a2621565c5c4efbe79177c2326a3ef8eef6e10922cbc025682c6bade7179add2e59b3b6394419da0cf7c82f1fd8dcfb58efe49f3
|
data/README.md
CHANGED
@@ -100,13 +100,15 @@ If the specified threshold is not met, an error code (1) is returned along with
|
|
100
100
|
|
101
101
|
The compliance score are rounded down to the nearest whole number. For example a score of 77.3 would be displayed as 77.
|
102
102
|
|
103
|
+
Thresholds provided inline (i.e. `-i`) override thresholds provided by files (i.e. `-f`).
|
104
|
+
|
103
105
|
```
|
104
106
|
USAGE: inspec_tools compliance [OPTIONS] -j <inspec-json> -i <threshold-inline>
|
105
107
|
inspec_tools compliance [OPTIONS] -j <inspec-json> -f <threshold-file>
|
106
108
|
FLAGS:
|
107
109
|
-j --inspec-json <inspec-json> : path to InSpec results Json
|
108
110
|
-i --template-inline <threshold-inline> : inline compliance threshold definition
|
109
|
-
-f --
|
111
|
+
-f --threshold-file <threshold-file> : yaml file with compliance threshold definition
|
110
112
|
Examples:
|
111
113
|
|
112
114
|
inspec_tools compliance -j examples/sample_json/rhel-simp.json -i '{compliance.min: 80, failed.critical.max: 0, failed.high.max: 0}'
|
@@ -135,11 +137,11 @@ failed.high.max: 1
|
|
135
137
|
|
136
138
|
#### In-Line Examples
|
137
139
|
```
|
138
|
-
{compliance: {min: 90}, failed: {critical: {max: 0}, high: {max: 0}}}
|
140
|
+
"{compliance: {min: 90}, failed: {critical: {max: 0}, high: {max: 0}}}"
|
139
141
|
```
|
140
142
|
|
141
143
|
```
|
142
|
-
{compliance.min: 81, failed.critical.max: 0, failed.high.max: 0}
|
144
|
+
"{compliance.min: 81, failed.critical.max: 0, failed.high.max: 0}"
|
143
145
|
```
|
144
146
|
|
145
147
|
## summary
|
@@ -185,12 +187,16 @@ Using additional flags will override the normal output and only display the outp
|
|
185
187
|
|
186
188
|
USAGE: inspec_tools summary [OPTIONS] -j <inspec-json>
|
187
189
|
|
190
|
+
Thresholds provided inline (i.e. `-i`) override thresholds provided by files (i.e. `-t`).
|
191
|
+
|
192
|
+
|
188
193
|
```
|
189
194
|
FLAGS:
|
190
|
-
-j --inspec-json <inspec-json>
|
191
|
-
-
|
192
|
-
-
|
193
|
-
-
|
195
|
+
-j --inspec-json <inspec-json> : path to InSpec results JSON
|
196
|
+
-f --json-full, --no-json-full : print the summary STDOUT as JSON
|
197
|
+
-k --json-counts, --no-json-counts : print the result status to STDOUT as JSON
|
198
|
+
-t, --threshold-file=THRESHOLD_FILE] : path to threshold YAML file
|
199
|
+
-i, --threshold-inline=THRESHOLD_INLINE] : string of text representing threshold YAML inline
|
194
200
|
|
195
201
|
Examples:
|
196
202
|
|
data/lib/inspec_tools/csv.rb
CHANGED
@@ -21,12 +21,12 @@ module InspecTools
|
|
21
21
|
@csv.shift if @mapping['skip_csv_header']
|
22
22
|
end
|
23
23
|
|
24
|
-
def to_inspec
|
24
|
+
def to_inspec(control_name_prefix: nil)
|
25
25
|
@controls = []
|
26
26
|
@profile = {}
|
27
27
|
@cci_xml = Utils::CciXml.get_cci_list('U_CCI_List.xml')
|
28
28
|
insert_metadata
|
29
|
-
parse_controls
|
29
|
+
parse_controls(control_name_prefix)
|
30
30
|
@profile['controls'] = @controls
|
31
31
|
@profile['sha256'] = Digest::SHA256.hexdigest(@profile.to_s)
|
32
32
|
@profile
|
@@ -66,12 +66,12 @@ module InspecTools
|
|
66
66
|
cell.split("\n").first
|
67
67
|
end
|
68
68
|
|
69
|
-
def parse_controls
|
69
|
+
def parse_controls(prefix)
|
70
70
|
@csv.each do |row|
|
71
71
|
control = {}
|
72
|
-
control['id'] = row[@mapping['control.id']]
|
73
|
-
control['title'] = row[@mapping['control.title']]
|
74
|
-
control['desc'] = row[@mapping['control.desc']]
|
72
|
+
control['id'] = generate_control_id(prefix, row[@mapping['control.id']]) unless @mapping['control.id'].nil? || row[@mapping['control.id']].nil?
|
73
|
+
control['title'] = row[@mapping['control.title']] unless @mapping['control.title'].nil? || row[@mapping['control.title']].nil?
|
74
|
+
control['desc'] = row[@mapping['control.desc']] unless @mapping['control.desc'].nil? || row[@mapping['control.desc']].nil?
|
75
75
|
control['tags'] = {}
|
76
76
|
cci_number = get_cci_number(row[@mapping['control.tags']['cci']])
|
77
77
|
nist = get_nist_reference(cci_number) unless cci_number.nil?
|
@@ -90,5 +90,15 @@ module InspecTools
|
|
90
90
|
@controls << control
|
91
91
|
end
|
92
92
|
end
|
93
|
+
|
94
|
+
def generate_control_id(prefix, id)
|
95
|
+
return id if prefix.nil?
|
96
|
+
|
97
|
+
"#{prefix}-#{id}"
|
98
|
+
end
|
93
99
|
end
|
94
100
|
end
|
101
|
+
|
102
|
+
# rubocop:enable Metrics/AbcSize
|
103
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
104
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
@@ -1,4 +1,3 @@
|
|
1
|
-
require 'yaml'
|
2
1
|
require 'json'
|
3
2
|
require 'roo'
|
4
3
|
require_relative '../utilities/inspec_util'
|
@@ -13,7 +12,6 @@ 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'
|
18
16
|
autoload :GenerateMap, 'inspec_tools/generate_map'
|
19
17
|
end
|
@@ -80,10 +78,11 @@ module InspecPlugins
|
|
80
78
|
option :output, required: false, aliases: '-o', default: 'profile'
|
81
79
|
option :format, required: false, aliases: '-f', enum: %w{ruby hash}, default: 'ruby'
|
82
80
|
option :separate_files, required: false, type: :boolean, default: true, aliases: '-s'
|
81
|
+
option :control_name_prefix, required: false, type: :string, aliases: '-p'
|
83
82
|
def csv2inspec
|
84
83
|
csv = CSV.read(options[:csv], encoding: 'ISO8859-1')
|
85
84
|
mapping = YAML.load_file(options[:mapping])
|
86
|
-
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])
|
87
86
|
Utils::InspecUtil.unpack_inspec_json(options[:output], profile, options[:separate_files], options[:format])
|
88
87
|
end
|
89
88
|
|
@@ -189,26 +188,14 @@ module InspecPlugins
|
|
189
188
|
desc 'summary', 'summary parses an inspec results json to create a summary json'
|
190
189
|
long_desc InspecTools::Help.text(:summary)
|
191
190
|
option :inspec_json, required: true, aliases: '-j'
|
192
|
-
option :verbose, type: :boolean, aliases: '-V'
|
193
191
|
option :json_full, type: :boolean, required: false, aliases: '-f'
|
194
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'
|
195
195
|
|
196
196
|
def summary
|
197
|
-
summary = InspecTools::Summary.new(
|
198
|
-
|
199
|
-
unless options.include?('json_full') || options.include?('json_counts')
|
200
|
-
puts "\nOverall compliance: #{summary[:compliance]}%\n\n"
|
201
|
-
summary[:status].keys.each do |category|
|
202
|
-
puts category
|
203
|
-
summary[:status][category].keys.each do |impact|
|
204
|
-
puts "\t#{impact} : #{summary[:status][category][impact]}"
|
205
|
-
end
|
206
|
-
end
|
207
|
-
end
|
208
|
-
|
209
|
-
json_summary = summary.to_json
|
210
|
-
puts json_summary if options[:json_full]
|
211
|
-
puts summary[:status].to_json if options[:json_counts]
|
197
|
+
summary = InspecTools::Summary.new(options: options)
|
198
|
+
summary.output_summary
|
212
199
|
end
|
213
200
|
|
214
201
|
desc 'compliance', 'compliance parses an inspec results json to check if the compliance level meets a specified threshold'
|
@@ -216,17 +203,10 @@ module InspecPlugins
|
|
216
203
|
option :inspec_json, required: true, aliases: '-j'
|
217
204
|
option :threshold_file, required: false, aliases: '-f'
|
218
205
|
option :threshold_inline, required: false, aliases: '-i'
|
219
|
-
option :verbose, type: :boolean, aliases: '-V'
|
220
206
|
|
221
207
|
def compliance
|
222
|
-
|
223
|
-
|
224
|
-
exit(1)
|
225
|
-
end
|
226
|
-
threshold = YAML.load_file(options[:threshold_file]) unless options[:threshold_file].nil?
|
227
|
-
threshold = YAML.safe_load(options[:threshold_inline]) unless options[:threshold_inline].nil?
|
228
|
-
compliance = InspecTools::Summary.new(File.read(options[:inspec_json])).threshold(threshold)
|
229
|
-
compliance ? exit(0) : exit(1)
|
208
|
+
compliance = InspecTools::Summary.new(options: options)
|
209
|
+
compliance.results_meet_threshold? ? exit(0) : exit(1)
|
230
210
|
end
|
231
211
|
end
|
232
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
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: inspec_tools
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Robert Thew
|
@@ -11,7 +11,7 @@ authors:
|
|
11
11
|
autorequire:
|
12
12
|
bindir: exe
|
13
13
|
cert_chain: []
|
14
|
-
date: 2020-
|
14
|
+
date: 2020-10-16 00:00:00.000000000 Z
|
15
15
|
dependencies:
|
16
16
|
- !ruby/object:Gem::Dependency
|
17
17
|
name: colorize
|