inspec_tools 2.0.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.md +15 -0
- data/README.md +373 -0
- data/Rakefile +96 -0
- data/exe/inspec_tools +14 -0
- data/lib/data/README.TXT +25 -0
- data/lib/data/U_CCI_List.xml +38403 -0
- data/lib/data/attributes.yml +23 -0
- data/lib/data/cci2html.xsl +136 -0
- data/lib/data/cis_to_nist_critical_controls +0 -0
- data/lib/data/cis_to_nist_mapping +0 -0
- data/lib/data/mapping.yml +17 -0
- data/lib/data/rubocop.yml +4 -0
- data/lib/data/stig.csv +1 -0
- data/lib/data/threshold.yaml +83 -0
- data/lib/exceptions/impact_input_error.rb +6 -0
- data/lib/exceptions/severity_input_error.rb +6 -0
- data/lib/happy_mapper_tools/benchmark.rb +161 -0
- data/lib/happy_mapper_tools/cci_attributes.rb +66 -0
- data/lib/happy_mapper_tools/stig_attributes.rb +216 -0
- data/lib/happy_mapper_tools/stig_checklist.rb +99 -0
- data/lib/inspec_tools.rb +17 -0
- data/lib/inspec_tools/ckl.rb +20 -0
- data/lib/inspec_tools/cli.rb +31 -0
- data/lib/inspec_tools/csv.rb +101 -0
- data/lib/inspec_tools/help.rb +9 -0
- data/lib/inspec_tools/help/compliance.md +7 -0
- data/lib/inspec_tools/help/csv2inspec.md +5 -0
- data/lib/inspec_tools/help/inspec2ckl.md +5 -0
- data/lib/inspec_tools/help/inspec2csv.md +5 -0
- data/lib/inspec_tools/help/inspec2xccdf.md +5 -0
- data/lib/inspec_tools/help/pdf2inspec.md +6 -0
- data/lib/inspec_tools/help/summary.md +5 -0
- data/lib/inspec_tools/help/xccdf2inspec.md +5 -0
- data/lib/inspec_tools/inspec.rb +331 -0
- data/lib/inspec_tools/pdf.rb +125 -0
- data/lib/inspec_tools/plugin.rb +15 -0
- data/lib/inspec_tools/plugin_cli.rb +275 -0
- data/lib/inspec_tools/summary.rb +126 -0
- data/lib/inspec_tools/version.rb +8 -0
- data/lib/inspec_tools/xccdf.rb +156 -0
- data/lib/inspec_tools/xlsx_tool.rb +135 -0
- data/lib/inspec_tools_plugin.rb +7 -0
- data/lib/overrides/false_class.rb +5 -0
- data/lib/overrides/nil_class.rb +5 -0
- data/lib/overrides/object.rb +5 -0
- data/lib/overrides/string.rb +5 -0
- data/lib/overrides/true_class.rb +5 -0
- data/lib/utilities/cis_to_nist.rb +11 -0
- data/lib/utilities/csv_util.rb +14 -0
- data/lib/utilities/extract_pdf_text.rb +20 -0
- data/lib/utilities/inspec_util.rb +441 -0
- data/lib/utilities/parser.rb +373 -0
- data/lib/utilities/text_cleaner.rb +69 -0
- metadata +359 -0
@@ -0,0 +1,125 @@
|
|
1
|
+
require 'digest'
|
2
|
+
|
3
|
+
require_relative '../utilities/inspec_util'
|
4
|
+
require_relative '../utilities/extract_pdf_text'
|
5
|
+
require_relative '../utilities/parser'
|
6
|
+
require_relative '../utilities/text_cleaner'
|
7
|
+
require_relative '../utilities/cis_to_nist'
|
8
|
+
|
9
|
+
# rubocop:disable Metrics/AbcSize
|
10
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
11
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
12
|
+
|
13
|
+
module InspecTools
|
14
|
+
class PDF
|
15
|
+
def initialize(pdf, profile_name, debug = false)
|
16
|
+
raise ArgumentError if pdf.nil?
|
17
|
+
|
18
|
+
@pdf = pdf
|
19
|
+
@name = profile_name
|
20
|
+
@debug = debug
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_inspec
|
24
|
+
@controls = []
|
25
|
+
@csv_handle = nil
|
26
|
+
@cci_xml = nil
|
27
|
+
@nist_mapping = Utils::CisToNist.get_mapping('cis_to_nist_critical_controls')
|
28
|
+
@pdf_text = ''
|
29
|
+
@clean_text = ''
|
30
|
+
@transformed_data = ''
|
31
|
+
@profile = {}
|
32
|
+
read_pdf
|
33
|
+
@title ||= extract_title
|
34
|
+
clean_pdf_text
|
35
|
+
transform_data
|
36
|
+
insert_json_metadata
|
37
|
+
@profile['controls'] = parse_controls
|
38
|
+
@profile['sha256'] = Digest::SHA256.hexdigest @profile.to_s
|
39
|
+
@profile
|
40
|
+
end
|
41
|
+
|
42
|
+
def to_csv
|
43
|
+
# TODO: to_csv
|
44
|
+
end
|
45
|
+
|
46
|
+
def to_xccdf
|
47
|
+
# TODO: to_xccdf
|
48
|
+
end
|
49
|
+
|
50
|
+
def to_ckl
|
51
|
+
# TODO: to_ckl
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
# converts passed in data into InSpec format
|
57
|
+
def parse_controls
|
58
|
+
controls = []
|
59
|
+
@transformed_data.each do |contr|
|
60
|
+
nist = find_nist(contr[:cis]) unless contr[:cis] == 'No CIS Control'
|
61
|
+
control = {}
|
62
|
+
control['id'] = 'M-' + contr[:title].split(' ')[0]
|
63
|
+
control['title'] = contr[:title]
|
64
|
+
control['desc'] = contr[:descr]
|
65
|
+
control['impact'] = Utils::InspecUtil.get_impact('medium')
|
66
|
+
control['tags'] = {}
|
67
|
+
control['tags']['severity'] = Utils::InspecUtil.get_impact_string(control['impact'])
|
68
|
+
control['tags']['ref'] = contr[:ref] unless contr[:ref].nil?
|
69
|
+
control['tags']['applicability'] = contr[:applicability] unless contr[:applicability].nil?
|
70
|
+
control['tags']['cis_id'] = contr[:title].split(' ')[0] unless contr[:title].nil?
|
71
|
+
control['tags']['cis_control'] = [contr[:cis], @nist_mapping[0][:cis_ver]] unless contr[:cis].nil? # tag cis_control: [5, 6.1] ##6.1 is the version
|
72
|
+
control['tags']['cis_level'] = contr[:level] unless contr[:level].nil?
|
73
|
+
control['tags']['nist'] = nist unless nist.nil? # tag nist: [AC-3, 4] ##4 is the version
|
74
|
+
control['tags']['check'] = contr[:check] unless contr[:check].nil?
|
75
|
+
control['tags']['fix'] = contr[:fix] unless contr[:fix].nil?
|
76
|
+
control['tags']['Default Value'] = contr[:default] unless contr[:default].nil?
|
77
|
+
controls << control
|
78
|
+
end
|
79
|
+
controls
|
80
|
+
end
|
81
|
+
|
82
|
+
def insert_json_metadata
|
83
|
+
@profile['name'] = @name
|
84
|
+
@profile['title'] = @title
|
85
|
+
@profile['maintainer'] = 'The Authors'
|
86
|
+
@profile['copyright'] = 'The Authors'
|
87
|
+
@profile['copyright_email'] = 'you@example.com'
|
88
|
+
@profile['license'] = 'Apache-2.0'
|
89
|
+
@profile['summary'] = 'An InSpec Compliance Profile'
|
90
|
+
@profile['version'] = '0.1.0'
|
91
|
+
@profile['supports'] = []
|
92
|
+
@profile['attributes'] = []
|
93
|
+
@profile['generator'] = {
|
94
|
+
'name': 'inspec_tools',
|
95
|
+
'version': VERSION
|
96
|
+
}
|
97
|
+
end
|
98
|
+
|
99
|
+
def extract_title
|
100
|
+
@pdf_text.match(/([^\n]*)\n/).captures[0]
|
101
|
+
end
|
102
|
+
|
103
|
+
def read_pdf
|
104
|
+
@pdf_text = Util::ExtractPdfText.new(@pdf).extracted_text
|
105
|
+
write_pdf_text if @debug
|
106
|
+
end
|
107
|
+
|
108
|
+
def clean_pdf_text
|
109
|
+
@clean_text = Util::TextCleaner.new.clean_data(@pdf_text)
|
110
|
+
write_clean_text if @debug
|
111
|
+
end
|
112
|
+
|
113
|
+
def transform_data
|
114
|
+
@transformed_data = Util::PrepareData.new(@clean_text).transformed_data
|
115
|
+
end
|
116
|
+
|
117
|
+
def write_pdf_text
|
118
|
+
File.write('pdf_text', @pdf_text)
|
119
|
+
end
|
120
|
+
|
121
|
+
def write_clean_text
|
122
|
+
File.write('debug_text', @clean_text)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module InspecToolsPlugin
|
4
|
+
class Plugin < Inspec.plugin(2)
|
5
|
+
# Metadata
|
6
|
+
# Must match entry in plugins.json
|
7
|
+
plugin_name :'inspec-tools_plugin'
|
8
|
+
|
9
|
+
# Activation hooks (CliCommand as an example)
|
10
|
+
cli_command :tools do
|
11
|
+
require_relative 'plugin_cli'
|
12
|
+
InspecPlugins::InspecToolsPlugin::CliCommand
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,275 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'json'
|
3
|
+
require 'roo'
|
4
|
+
require_relative '../utilities/inspec_util'
|
5
|
+
require_relative '../utilities/csv_util'
|
6
|
+
|
7
|
+
module InspecTools
|
8
|
+
autoload :Help, 'inspec_tools/help'
|
9
|
+
autoload :Command, 'inspec_tools/command'
|
10
|
+
autoload :XCCDF, 'inspec_tools/xccdf'
|
11
|
+
autoload :PDF, 'inspec_tools/pdf'
|
12
|
+
autoload :CSVTool, 'inspec_tools/csv'
|
13
|
+
autoload :CKL, 'inspec_tools/ckl'
|
14
|
+
autoload :Inspec, 'inspec_tools/inspec'
|
15
|
+
autoload :Summary, 'inspec_tools/summary'
|
16
|
+
autoload :Threshold, 'inspec_tools/threshold'
|
17
|
+
autoload :XLSXTool, 'inspec_tools/xlsx_tool'
|
18
|
+
end
|
19
|
+
|
20
|
+
# rubocop:disable Style/GuardClause
|
21
|
+
module InspecPlugins
|
22
|
+
module InspecToolsPlugin
|
23
|
+
class CliCommand < Inspec.plugin(2, :cli_command) # rubocop:disable Metrics/ClassLength
|
24
|
+
POSSIBLE_LOG_LEVELS = %w{debug info warn error fatal}.freeze
|
25
|
+
|
26
|
+
class_option :log_directory, type: :string, aliases: :l, desc: 'Provie log location'
|
27
|
+
class_option :log_level, type: :string, desc: "Set the logging level: #{POSSIBLE_LOG_LEVELS}"
|
28
|
+
|
29
|
+
subcommand_desc 'tools [COMMAND]', 'Runs inspec_tools commands through Inspec'
|
30
|
+
|
31
|
+
desc 'xccdf2inspec', 'xccdf2inspec translates an xccdf file to an inspec profile'
|
32
|
+
long_desc InspecTools::Help.text(:xccdf2inspec)
|
33
|
+
option :xccdf, required: true, aliases: '-x'
|
34
|
+
option :attributes, required: false, aliases: '-a'
|
35
|
+
option :output, required: false, aliases: '-o', default: 'profile'
|
36
|
+
option :format, required: false, aliases: '-f', enum: %w{ruby hash}, default: 'ruby'
|
37
|
+
option :separate_files, required: false, type: :boolean, default: true, aliases: '-s'
|
38
|
+
option :replace_tags, type: :array, required: false, aliases: '-r'
|
39
|
+
option :metadata, required: false, aliases: '-m'
|
40
|
+
def xccdf2inspec
|
41
|
+
xccdf = InspecTools::XCCDF.new(File.read(options[:xccdf]), options[:replace_tags])
|
42
|
+
profile = xccdf.to_inspec
|
43
|
+
|
44
|
+
if !options[:metadata].nil?
|
45
|
+
xccdf.inject_metadata(File.read(options[:metadata]))
|
46
|
+
end
|
47
|
+
|
48
|
+
Utils::InspecUtil.unpack_inspec_json(options[:output], profile, options[:separate_files], options[:format])
|
49
|
+
if !options[:attributes].nil?
|
50
|
+
attributes = xccdf.to_attributes
|
51
|
+
File.write(options[:attributes], YAML.dump(attributes))
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
desc 'inspec2xccdf', 'inspec2xccdf translates an inspec profile and attributes files to an xccdf file'
|
56
|
+
long_desc InspecTools::Help.text(:inspec2xccdf)
|
57
|
+
option :inspec_json, required: true, aliases: '-j'
|
58
|
+
option :attributes, required: true, aliases: '-a'
|
59
|
+
option :output, required: true, aliases: '-o'
|
60
|
+
def inspec2xccdf
|
61
|
+
json = File.read(options[:inspec_json])
|
62
|
+
inspec_tool = InspecTools::Inspec.new(json)
|
63
|
+
attr_hsh = YAML.load_file(options[:attributes])
|
64
|
+
xccdf = inspec_tool.to_xccdf(attr_hsh)
|
65
|
+
File.write(options[:output], xccdf)
|
66
|
+
end
|
67
|
+
|
68
|
+
desc 'csv2inspec', 'csv2inspec translates CSV to Inspec controls using a mapping file'
|
69
|
+
long_desc InspecTools::Help.text(:csv2inspec)
|
70
|
+
option :csv, required: true, aliases: '-c'
|
71
|
+
option :mapping, required: true, aliases: '-m'
|
72
|
+
option :verbose, required: false, type: :boolean, aliases: '-V'
|
73
|
+
option :output, required: false, aliases: '-o', default: 'profile'
|
74
|
+
option :format, required: false, aliases: '-f', enum: %w{ruby hash}, default: 'ruby'
|
75
|
+
option :separate_files, required: false, type: :boolean, default: true, aliases: '-s'
|
76
|
+
def csv2inspec
|
77
|
+
csv = CSV.read(options[:csv], encoding: 'ISO8859-1')
|
78
|
+
mapping = YAML.load_file(options[:mapping])
|
79
|
+
profile = InspecTools::CSVTool.new(csv, mapping, options[:csv].split('/')[-1].split('.')[0], options[:verbose]).to_inspec
|
80
|
+
Utils::InspecUtil.unpack_inspec_json(options[:output], profile, options[:separate_files], options[:format])
|
81
|
+
end
|
82
|
+
|
83
|
+
desc 'xlsx2inspec', 'xlsx2inspec translates CIS XLSX to Inspec controls using a mapping file'
|
84
|
+
long_desc InspecTools::Help.text(:xlsx2inspec)
|
85
|
+
option :xlsx, required: true, aliases: '-x'
|
86
|
+
option :mapping, required: true, aliases: '-m'
|
87
|
+
option :control_name_prefix, required: true, aliases: '-p'
|
88
|
+
option :verbose, required: false, type: :boolean, aliases: '-V'
|
89
|
+
option :output, required: false, aliases: '-o', default: 'profile'
|
90
|
+
option :format, required: false, aliases: '-f', enum: %w{ruby hash}, default: 'ruby'
|
91
|
+
option :separate_files, required: false, type: :boolean, default: true, aliases: '-s'
|
92
|
+
def xlsx2inspec
|
93
|
+
xlsx = Roo::Spreadsheet.open(options[:xlsx])
|
94
|
+
mapping = YAML.load_file(options[:mapping])
|
95
|
+
profile = InspecTools::XLSXTool.new(xlsx, mapping, options[:xlsx].split('/')[-1].split('.')[0], options[:verbose]).to_inspec(options[:control_name_prefix])
|
96
|
+
Utils::InspecUtil.unpack_inspec_json(options[:output], profile, options[:separate_files], options[:format])
|
97
|
+
end
|
98
|
+
|
99
|
+
desc 'inspec2csv', 'inspec2csv translates Inspec controls to CSV'
|
100
|
+
long_desc InspecTools::Help.text(:inspec2csv)
|
101
|
+
option :inspec_json, required: true, aliases: '-j'
|
102
|
+
option :output, required: true, aliases: '-o'
|
103
|
+
option :verbose, required: false, type: :boolean, aliases: '-V'
|
104
|
+
def inspec2csv
|
105
|
+
csv = InspecTools::Inspec.new(File.read(options[:inspec_json])).to_csv
|
106
|
+
Utils::CSVUtil.unpack_csv(csv, options[:output])
|
107
|
+
end
|
108
|
+
|
109
|
+
desc 'inspec2ckl', 'inspec2ckl translates an inspec json file to a Checklist file'
|
110
|
+
long_desc InspecTools::Help.text(:inspec2ckl)
|
111
|
+
option :inspec_json, required: true, aliases: '-j'
|
112
|
+
option :output, required: true, aliases: '-o'
|
113
|
+
option :verbose, type: :boolean, aliases: '-V'
|
114
|
+
option :metadata, required: false, aliases: '-m'
|
115
|
+
def inspec2ckl
|
116
|
+
metadata = '{}'
|
117
|
+
if !options[:metadata].nil?
|
118
|
+
metadata = File.read(options[:metadata])
|
119
|
+
end
|
120
|
+
ckl = InspecTools::Inspec.new(File.read(options[:inspec_json]), metadata).to_ckl
|
121
|
+
File.write(options[:output], ckl)
|
122
|
+
end
|
123
|
+
|
124
|
+
desc 'pdf2inspec', 'pdf2inspec translates a PDF Security Control Speficication to Inspec Security Profile'
|
125
|
+
long_desc InspecTools::Help.text(:pdf2inspec)
|
126
|
+
option :pdf, required: true, aliases: '-p'
|
127
|
+
option :output, required: false, aliases: '-o', default: 'profile'
|
128
|
+
option :debug, required: false, aliases: '-d', type: :boolean, default: false
|
129
|
+
option :format, required: false, aliases: '-f', enum: %w{ruby hash}, default: 'ruby'
|
130
|
+
option :separate_files, required: false, type: :boolean, default: true, aliases: '-s'
|
131
|
+
def pdf2inspec
|
132
|
+
pdf = File.open(options[:pdf])
|
133
|
+
profile = InspecTools::PDF.new(pdf, options[:output], options[:debug]).to_inspec
|
134
|
+
Utils::InspecUtil.unpack_inspec_json(options[:output], profile, options[:separate_files], options[:format])
|
135
|
+
end
|
136
|
+
|
137
|
+
desc 'generate_map', 'Generates mapping template from CSV to Inspec Controls'
|
138
|
+
def generate_map
|
139
|
+
template = '
|
140
|
+
# Setting csv_header to true will skip the csv file header
|
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
|
159
|
+
end
|
160
|
+
|
161
|
+
desc 'generate_ckl_metadata', 'Generate metadata file that can be passed to inspec2ckl'
|
162
|
+
def generate_ckl_metadata
|
163
|
+
metadata = {}
|
164
|
+
|
165
|
+
metadata['stigid'] = ask('STID ID: ')
|
166
|
+
metadata['role'] = ask('Role: ')
|
167
|
+
metadata['type'] = ask('Type: ')
|
168
|
+
metadata['hostname'] = ask('Hostname: ')
|
169
|
+
metadata['ip'] = ask('IP Address: ')
|
170
|
+
metadata['mac'] = ask('MAC Address: ')
|
171
|
+
metadata['fqdn'] = ask('FQDN: ')
|
172
|
+
metadata['tech_area'] = ask('Tech Area: ')
|
173
|
+
metadata['target_key'] = ask('Target Key: ')
|
174
|
+
metadata['web_or_database'] = ask('Web or Database: ')
|
175
|
+
metadata['web_db_site'] = ask('Web DB Site: ')
|
176
|
+
metadata['web_db_instance'] = ask('Web DB Instance: ')
|
177
|
+
|
178
|
+
metadata.delete_if { |_key, value| value.empty? }
|
179
|
+
File.open('metadata.json', 'w') do |f|
|
180
|
+
f.write(metadata.to_json)
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
desc 'generate_inspec_metadata', 'Generate mapping file that can be passed to xccdf2inspec'
|
185
|
+
def generate_inspec_metadata
|
186
|
+
metadata = {}
|
187
|
+
|
188
|
+
metadata['maintainer'] = ask('Maintainer: ')
|
189
|
+
metadata['copyright'] = ask('Copyright: ')
|
190
|
+
metadata['copyright_email'] = ask('Copyright Email: ')
|
191
|
+
metadata['license'] = ask('License: ')
|
192
|
+
metadata['version'] = ask('Version: ')
|
193
|
+
|
194
|
+
metadata.delete_if { |_key, value| value.empty? }
|
195
|
+
File.open('metadata.json', 'w') do |f|
|
196
|
+
f.write(metadata.to_json)
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
desc 'summary', 'summary parses an inspec results json to create a summary json'
|
201
|
+
long_desc InspecTools::Help.text(:summary)
|
202
|
+
option :inspec_json, required: true, aliases: '-j'
|
203
|
+
option :verbose, type: :boolean, aliases: '-V'
|
204
|
+
option :json_full, type: :boolean, required: false, aliases: '-f'
|
205
|
+
option :json_counts, type: :boolean, required: false, aliases: '-k'
|
206
|
+
|
207
|
+
def summary
|
208
|
+
summary = InspecTools::Summary.new(File.read(options[:inspec_json])).to_summary
|
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]
|
223
|
+
end
|
224
|
+
|
225
|
+
desc 'compliance', 'compliance parses an inspec results json to check if the compliance level meets a specified threshold'
|
226
|
+
long_desc InspecTools::Help.text(:compliance)
|
227
|
+
option :inspec_json, required: true, aliases: '-j'
|
228
|
+
option :threshold_file, required: false, aliases: '-f'
|
229
|
+
option :threshold_inline, required: false, aliases: '-i'
|
230
|
+
option :verbose, type: :boolean, aliases: '-V'
|
231
|
+
|
232
|
+
def compliance
|
233
|
+
if options[:threshold_file].nil? && options[:threshold_inline].nil?
|
234
|
+
puts 'Please provide threshold as a yaml file or inline yaml'
|
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)
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
#=====================================================================#
|
247
|
+
# Pre-Flight Code
|
248
|
+
#=====================================================================#
|
249
|
+
help_commands = ['-h', '--help', 'help']
|
250
|
+
log_commands = ['-l', '--log-directory']
|
251
|
+
version_commands = ['-v', '--version', 'version']
|
252
|
+
|
253
|
+
#---------------------------------------------------------------------#
|
254
|
+
# Adjustments for non-required version commands
|
255
|
+
#---------------------------------------------------------------------#
|
256
|
+
unless (version_commands & ARGV).empty?
|
257
|
+
puts InspecTools::VERSION
|
258
|
+
exit 0
|
259
|
+
end
|
260
|
+
|
261
|
+
#---------------------------------------------------------------------#
|
262
|
+
# Adjustments for non-required log-directory
|
263
|
+
#---------------------------------------------------------------------#
|
264
|
+
ARGV.push("--log-directory=#{Dir.pwd}/logs") if (log_commands & ARGV).empty? && (help_commands & ARGV).empty?
|
265
|
+
|
266
|
+
# Push help to front of command so thor recognizes subcommands are called with help
|
267
|
+
if help_commands.any? { |cmd| ARGV.include? cmd }
|
268
|
+
help_commands.each do |cmd|
|
269
|
+
if (match = ARGV.delete(cmd))
|
270
|
+
ARGV.unshift match
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
# rubocop:enable Style/GuardClause
|
@@ -0,0 +1,126 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'yaml'
|
3
|
+
require_relative '../utilities/inspec_util'
|
4
|
+
|
5
|
+
# rubocop:disable Metrics/AbcSize
|
6
|
+
|
7
|
+
# Impact Definitions
|
8
|
+
CRITICAL = 0.9
|
9
|
+
HIGH = 0.7
|
10
|
+
MEDIUM = 0.5
|
11
|
+
LOW = 0.3
|
12
|
+
|
13
|
+
BUCKETS = %i(failed passed no_impact skipped error).freeze
|
14
|
+
TALLYS = %i(total critical high medium low).freeze
|
15
|
+
|
16
|
+
THRESHOLD_TEMPLATE = File.expand_path('../data/threshold.yaml', File.dirname(__FILE__))
|
17
|
+
|
18
|
+
module InspecTools
|
19
|
+
class Summary
|
20
|
+
def initialize(inspec_json)
|
21
|
+
@json = JSON.parse(inspec_json)
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_summary
|
25
|
+
@data = Utils::InspecUtil.parse_data_for_ckl(@json)
|
26
|
+
@summary = {}
|
27
|
+
@data.keys.each do |control_id|
|
28
|
+
current_control = @data[control_id]
|
29
|
+
current_control[:compliance_status] = Utils::InspecUtil.control_status(current_control, true)
|
30
|
+
current_control[:finding_details] = Utils::InspecUtil.control_finding_details(current_control, current_control[:compliance_status])
|
31
|
+
end
|
32
|
+
compute_summary
|
33
|
+
@summary
|
34
|
+
end
|
35
|
+
|
36
|
+
def threshold(threshold = nil)
|
37
|
+
@summary = to_summary
|
38
|
+
@threshold = Utils::InspecUtil.to_dotted_hash(YAML.load_file(THRESHOLD_TEMPLATE))
|
39
|
+
parse_threshold(Utils::InspecUtil.to_dotted_hash(threshold))
|
40
|
+
threshold_compliance
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def compute_summary
|
46
|
+
@summary[:buckets] = {}
|
47
|
+
@summary[:buckets][:failed] = select_by_status(@data, 'Open')
|
48
|
+
@summary[:buckets][:passed] = select_by_status(@data, 'NotAFinding')
|
49
|
+
@summary[:buckets][:no_impact] = select_by_status(@data, 'Not_Applicable')
|
50
|
+
@summary[:buckets][:skipped] = select_by_status(@data, 'Not_Reviewed')
|
51
|
+
@summary[:buckets][:error] = select_by_status(@data, 'Profile_Error')
|
52
|
+
|
53
|
+
@summary[:status] = {}
|
54
|
+
@summary[:status][:failed] = tally_by_impact(@summary[:buckets][:failed])
|
55
|
+
@summary[:status][:passed] = tally_by_impact(@summary[:buckets][:passed])
|
56
|
+
@summary[:status][:no_impact] = tally_by_impact(@summary[:buckets][:no_impact])
|
57
|
+
@summary[:status][:skipped] = tally_by_impact(@summary[:buckets][:skipped])
|
58
|
+
@summary[:status][:error] = tally_by_impact(@summary[:buckets][:error])
|
59
|
+
|
60
|
+
@summary[:compliance] = compute_compliance
|
61
|
+
end
|
62
|
+
|
63
|
+
def select_by_impact(controls, impact)
|
64
|
+
controls.select { |_key, value| value[:impact].to_f.eql?(impact) }
|
65
|
+
end
|
66
|
+
|
67
|
+
def select_by_status(controls, status)
|
68
|
+
controls.select { |_key, value| value[:compliance_status].eql?(status) }
|
69
|
+
end
|
70
|
+
|
71
|
+
def tally_by_impact(controls)
|
72
|
+
tally = {}
|
73
|
+
tally[:total] = controls.count
|
74
|
+
tally[:critical] = select_by_impact(controls, CRITICAL).count
|
75
|
+
tally[:high] = select_by_impact(controls, HIGH).count
|
76
|
+
tally[:medium] = select_by_impact(controls, MEDIUM).count
|
77
|
+
tally[:low] = select_by_impact(controls, LOW).count
|
78
|
+
tally
|
79
|
+
end
|
80
|
+
|
81
|
+
def compute_compliance
|
82
|
+
(@summary[:status][:passed][:total]*100.0/
|
83
|
+
(@summary[:status][:passed][:total]+
|
84
|
+
@summary[:status][:failed][:total]+
|
85
|
+
@summary[:status][:skipped][:total]+
|
86
|
+
@summary[:status][:error][:total])).floor
|
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)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|