inspec_tools 0.0.0.1.ENOTAG
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 +7 -0
- data/CHANGELOG.md +662 -0
- data/LICENSE.md +15 -0
- data/README.md +329 -0
- data/Rakefile +30 -0
- data/exe/inspec_tools +14 -0
- 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/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/mapping.yml +17 -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 +196 -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 +136 -0
- data/lib/inspec_tools/plugin.rb +15 -0
- data/lib/inspec_tools/plugin_cli.rb +278 -0
- data/lib/inspec_tools/summary.rb +126 -0
- data/lib/inspec_tools/version.rb +8 -0
- data/lib/inspec_tools/xccdf.rb +155 -0
- data/lib/inspec_tools/xlsx_tool.rb +148 -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/csv_util.rb +14 -0
- data/lib/utilities/extract_nist_cis_mapping.rb +57 -0
- data/lib/utilities/extract_pdf_text.rb +20 -0
- data/lib/utilities/inspec_util.rb +435 -0
- data/lib/utilities/parser.rb +373 -0
- data/lib/utilities/text_cleaner.rb +69 -0
- metadata +359 -0
@@ -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,278 @@
|
|
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 :output, required: false, aliases: '-o'
|
204
|
+
option :cli, type: :boolean, required: false, aliases: '-c'
|
205
|
+
option :verbose, type: :boolean, aliases: '-V'
|
206
|
+
option :json_full, type: :boolean, required: false, aliases: '-f'
|
207
|
+
option :json_counts, type: :boolean, required: false, aliases: '-k'
|
208
|
+
|
209
|
+
def summary
|
210
|
+
summary = InspecTools::Summary.new(File.read(options[:inspec_json])).to_summary
|
211
|
+
|
212
|
+
if options[:cli]
|
213
|
+
puts "\nOverall compliance: #{summary[:compliance]}%\n\n"
|
214
|
+
summary[:status].keys.each do |category|
|
215
|
+
puts category
|
216
|
+
summary[:status][category].keys.each do |impact|
|
217
|
+
puts "\t#{impact} : #{summary[:status][category][impact]}"
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
json_summary = summary.to_json
|
223
|
+
File.write(options[:output], json_summary) if options[:output]
|
224
|
+
puts json_summary if options[:json_full]
|
225
|
+
puts summary[:status].to_json if options[:json_counts]
|
226
|
+
end
|
227
|
+
|
228
|
+
desc 'compliance', 'compliance parses an inspec results json to check if the compliance level meets a specified threshold'
|
229
|
+
long_desc InspecTools::Help.text(:compliance)
|
230
|
+
option :inspec_json, required: true, aliases: '-j'
|
231
|
+
option :threshold_file, required: false, aliases: '-f'
|
232
|
+
option :threshold_inline, required: false, aliases: '-i'
|
233
|
+
option :verbose, type: :boolean, aliases: '-V'
|
234
|
+
|
235
|
+
def compliance
|
236
|
+
if options[:threshold_file].nil? && options[:threshold_inline].nil?
|
237
|
+
puts 'Please provide threshold as a yaml file or inline yaml'
|
238
|
+
exit(1)
|
239
|
+
end
|
240
|
+
threshold = YAML.load_file(options[:threshold_file]) unless options[:threshold_file].nil?
|
241
|
+
threshold = YAML.safe_load(options[:threshold_inline]) unless options[:threshold_inline].nil?
|
242
|
+
compliance = InspecTools::Summary.new(File.read(options[:inspec_json])).threshold(threshold)
|
243
|
+
compliance ? exit(0) : exit(1)
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
#=====================================================================#
|
250
|
+
# Pre-Flight Code
|
251
|
+
#=====================================================================#
|
252
|
+
help_commands = ['-h', '--help', 'help']
|
253
|
+
log_commands = ['-l', '--log-directory']
|
254
|
+
version_commands = ['-v', '--version', 'version']
|
255
|
+
|
256
|
+
#---------------------------------------------------------------------#
|
257
|
+
# Adjustments for non-required version commands
|
258
|
+
#---------------------------------------------------------------------#
|
259
|
+
unless (version_commands & ARGV).empty?
|
260
|
+
puts InspecTools::VERSION
|
261
|
+
exit 0
|
262
|
+
end
|
263
|
+
|
264
|
+
#---------------------------------------------------------------------#
|
265
|
+
# Adjustments for non-required log-directory
|
266
|
+
#---------------------------------------------------------------------#
|
267
|
+
ARGV.push("--log-directory=#{Dir.pwd}/logs") if (log_commands & ARGV).empty? && (help_commands & ARGV).empty?
|
268
|
+
|
269
|
+
# Push help to front of command so thor recognizes subcommands are called with help
|
270
|
+
if help_commands.any? { |cmd| ARGV.include? cmd }
|
271
|
+
help_commands.each do |cmd|
|
272
|
+
if (match = ARGV.delete(cmd))
|
273
|
+
ARGV.unshift match
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
# 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 = %w{failed passed no_impact skipped error}.freeze
|
14
|
+
TALLYS = %w{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])).round(1)
|
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.to_sym][tally.to_sym] > max
|
108
|
+
compliance = false
|
109
|
+
failure << "Expected #{bucket}.#{tally}.max:#{max} got:#{status[bucket.to_sym][tally.to_sym]}"
|
110
|
+
end
|
111
|
+
if min != -1 and status[bucket.to_sym][tally.to_sym] < min
|
112
|
+
compliance = false
|
113
|
+
failure << "Expected #{bucket}.#{tally}.min:#{min} got:#{status[bucket.to_sym][tally.to_sym]}"
|
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
|
@@ -0,0 +1,155 @@
|
|
1
|
+
require_relative '../happy_mapper_tools/stig_attributes'
|
2
|
+
require_relative '../happy_mapper_tools/cci_attributes'
|
3
|
+
require_relative '../utilities/inspec_util'
|
4
|
+
|
5
|
+
require 'digest'
|
6
|
+
require 'json'
|
7
|
+
|
8
|
+
module InspecTools
|
9
|
+
# rubocop:disable Metrics/ClassLength
|
10
|
+
# rubocop:disable Metrics/AbcSize
|
11
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
12
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
13
|
+
# rubocop:disable Metrics/BlockLength
|
14
|
+
class XCCDF
|
15
|
+
def initialize(xccdf, replace_tags = nil)
|
16
|
+
@xccdf = xccdf
|
17
|
+
@xccdf = replace_tags_in_xccdf(replace_tags, @xccdf) unless replace_tags.nil?
|
18
|
+
cci_list_path = File.join(File.dirname(__FILE__), '../data/U_CCI_List.xml')
|
19
|
+
@cci_items = HappyMapperTools::CCIAttributes::CCI_List.parse(File.read(cci_list_path))
|
20
|
+
# @cci_items = HappyMapperTools::CCIAttributes::CCI_List.parse(File.read('./data/U_CCI_List.xml'))
|
21
|
+
@benchmark = HappyMapperTools::StigAttributes::Benchmark.parse(@xccdf)
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_ckl
|
25
|
+
# TODO: to_ckl
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_csv
|
29
|
+
# TODO: to_csv
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_inspec
|
33
|
+
@profile = {}
|
34
|
+
@controls = []
|
35
|
+
insert_json_metadata
|
36
|
+
insert_controls
|
37
|
+
@profile['sha256'] = Digest::SHA256.hexdigest @profile.to_s
|
38
|
+
@profile
|
39
|
+
end
|
40
|
+
|
41
|
+
####
|
42
|
+
# extracts non-InSpec attributes
|
43
|
+
###
|
44
|
+
# TODO there may be more attributes we want to extract, see data/attributes.yml for example
|
45
|
+
def to_attributes # rubocop:disable Metrics/AbcSize
|
46
|
+
@attribute = {}
|
47
|
+
|
48
|
+
@attribute['benchmark.title'] = @benchmark.title
|
49
|
+
@attribute['benchmark.id'] = @benchmark.id
|
50
|
+
@attribute['benchmark.description'] = @benchmark.description
|
51
|
+
@attribute['benchmark.version'] = @benchmark.version
|
52
|
+
|
53
|
+
@attribute['benchmark.status'] = @benchmark.status
|
54
|
+
@attribute['benchmark.status.date'] = @benchmark.release_date.release_date
|
55
|
+
|
56
|
+
@attribute['benchmark.notice.id'] = @benchmark.notice.id
|
57
|
+
|
58
|
+
@attribute['benchmark.plaintext'] = @benchmark.plaintext.plaintext
|
59
|
+
@attribute['benchmark.plaintext.id'] = @benchmark.plaintext.id
|
60
|
+
|
61
|
+
@attribute['reference.href'] = @benchmark.reference.href
|
62
|
+
@attribute['reference.dc.publisher'] = @benchmark.reference.dc_publisher
|
63
|
+
@attribute['reference.dc.source'] = @benchmark.reference.dc_source
|
64
|
+
@attribute['reference.dc.title'] = @benchmark.group[0].rule.reference.dc_title if !@benchmark.group[0].nil?
|
65
|
+
@attribute['reference.dc.subject'] = @benchmark.group[0].rule.reference.dc_subject if !@benchmark.group[0].nil?
|
66
|
+
@attribute['reference.dc.type'] = @benchmark.group[0].rule.reference.dc_type if !@benchmark.group[0].nil?
|
67
|
+
@attribute['reference.dc.identifier'] = @benchmark.group[0].rule.reference.dc_identifier if !@benchmark.group[0].nil?
|
68
|
+
|
69
|
+
@attribute['content_ref.name'] = @benchmark.group[0].rule.check.content_ref.name if !@benchmark.group[0].nil?
|
70
|
+
@attribute['content_ref.href'] = @benchmark.group[0].rule.check.content_ref.href if !@benchmark.group[0].nil?
|
71
|
+
|
72
|
+
@attribute
|
73
|
+
end
|
74
|
+
|
75
|
+
def publisher
|
76
|
+
@benchmark.reference.dc_publisher
|
77
|
+
end
|
78
|
+
|
79
|
+
def published
|
80
|
+
@benchmark.release_date.release_date
|
81
|
+
end
|
82
|
+
|
83
|
+
def inject_metadata(metadata = '{}')
|
84
|
+
json_metadata = JSON.parse(metadata)
|
85
|
+
json_metadata.each do |key, value|
|
86
|
+
@profile[key] = value
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
def replace_tags_in_xccdf(replace_tags, xccdf_xml)
|
93
|
+
replace_tags.each do |tag|
|
94
|
+
xccdf_xml = xccdf_xml.gsub(/(<|<)#{tag}(>|>)/, "$#{tag}")
|
95
|
+
end
|
96
|
+
xccdf_xml
|
97
|
+
end
|
98
|
+
|
99
|
+
def insert_json_metadata
|
100
|
+
@profile['name'] = @benchmark.id
|
101
|
+
@profile['title'] = @benchmark.title
|
102
|
+
@profile['maintainer'] = 'The Authors' if @profile['maintainer'].nil?
|
103
|
+
@profile['copyright'] = 'The Authors' if @profile['copyright'].nil?
|
104
|
+
@profile['copyright_email'] = 'you@example.com' if @profile['copyright_email'].nil?
|
105
|
+
@profile['license'] = 'Apache-2.0' if @profile['license'].nil?
|
106
|
+
@profile['summary'] = "\"#{@benchmark.description.gsub('\\', '\\\\\\').gsub('"', '\"')}\""
|
107
|
+
@profile['version'] = '0.1.0' if @profile['version'].nil?
|
108
|
+
@profile['supports'] = []
|
109
|
+
@profile['attributes'] = []
|
110
|
+
@profile['generator'] = {
|
111
|
+
'name': 'inspec_tools',
|
112
|
+
'version': VERSION
|
113
|
+
}
|
114
|
+
@profile['plaintext'] = @benchmark.plaintext.plaintext
|
115
|
+
@profile['status'] = "#{@benchmark.status} on #{@benchmark.release_date.release_date}"
|
116
|
+
@profile['reference_href'] = @benchmark.reference.href
|
117
|
+
@profile['reference_publisher'] = @benchmark.reference.dc_publisher
|
118
|
+
@profile['reference_source'] = @benchmark.reference.dc_source
|
119
|
+
end
|
120
|
+
|
121
|
+
def insert_controls
|
122
|
+
@benchmark.group.each do |group|
|
123
|
+
control = {}
|
124
|
+
control['id'] = group.id
|
125
|
+
control['title'] = group.rule.title
|
126
|
+
control['desc'] = group.rule.description.vuln_discussion.split('Satisfies: ')[0]
|
127
|
+
control['impact'] = Utils::InspecUtil.get_impact(group.rule.severity)
|
128
|
+
control['tags'] = {}
|
129
|
+
control['tags']['severity'] = Utils::InspecUtil.get_impact_string(control['impact'])
|
130
|
+
control['tags']['gtitle'] = group.title
|
131
|
+
control['tags']['satisfies'] = group.rule.description.vuln_discussion.split('Satisfies: ')[1].split(',').map(&:strip) if group.rule.description.vuln_discussion.split('Satisfies: ').length > 1
|
132
|
+
control['tags']['gid'] = group.id
|
133
|
+
control['tags']['rid'] = group.rule.id
|
134
|
+
control['tags']['stig_id'] = group.rule.version
|
135
|
+
control['tags']['fix_id'] = group.rule.fix.id
|
136
|
+
control['tags']['cci'] = group.rule.idents
|
137
|
+
control['tags']['nist'] = @cci_items.fetch_nists(group.rule.idents)
|
138
|
+
control['tags']['false_negatives'] = group.rule.description.false_negatives if group.rule.description.false_negatives != ''
|
139
|
+
control['tags']['false_positives'] = group.rule.description.false_positives if group.rule.description.false_positives != ''
|
140
|
+
control['tags']['documentable'] = group.rule.description.documentable if group.rule.description.documentable != ''
|
141
|
+
control['tags']['mitigations'] = group.rule.description.false_negatives if group.rule.description.mitigations != ''
|
142
|
+
control['tags']['severity_override_guidance'] = group.rule.description.severity_override_guidance if group.rule.description.severity_override_guidance != ''
|
143
|
+
control['tags']['potential_impacts'] = group.rule.description.potential_impacts if group.rule.description.potential_impacts != ''
|
144
|
+
control['tags']['third_party_tools'] = group.rule.description.third_party_tools if group.rule.description.third_party_tools != ''
|
145
|
+
control['tags']['mitigation_controls'] = group.rule.description.mitigation_controls if group.rule.description.mitigation_controls != ''
|
146
|
+
control['tags']['responsibility'] = group.rule.description.responsibility if group.rule.description.responsibility != ''
|
147
|
+
control['tags']['ia_controls'] = group.rule.description.ia_controls if group.rule.description.ia_controls != ''
|
148
|
+
control['tags']['check'] = group.rule.check.content
|
149
|
+
control['tags']['fix'] = group.rule.fixtext
|
150
|
+
@controls << control
|
151
|
+
end
|
152
|
+
@profile['controls'] = @controls
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|