inspec_tools 0.0.0.1.ENOTAG → 1.2.0

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.
Files changed (42) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +12 -657
  3. data/Guardfile +4 -0
  4. data/README.md +65 -132
  5. data/Rakefile +0 -6
  6. data/exe/inspec_tools +1 -1
  7. data/lib/data/README.TXT +1 -1
  8. data/lib/data/debug_text +5941 -0
  9. data/lib/happy_mapper_tools/cci_attributes.rb +22 -12
  10. data/lib/happy_mapper_tools/stig_checklist.rb +1 -6
  11. data/lib/inspec_tools.rb +2 -1
  12. data/lib/inspec_tools/cli.rb +140 -24
  13. data/lib/inspec_tools/command.rb +50 -0
  14. data/lib/inspec_tools/csv.rb +4 -6
  15. data/lib/inspec_tools/help/summary.md +2 -2
  16. data/lib/inspec_tools/inspec.rb +34 -133
  17. data/lib/inspec_tools/pdf.rb +2 -3
  18. data/lib/inspec_tools/summary.rb +2 -2
  19. data/lib/inspec_tools/version.rb +1 -6
  20. data/lib/inspec_tools/xccdf.rb +8 -22
  21. data/lib/utilities/inspec_util.rb +59 -208
  22. data/test/unit/inspec_tools/csv_test.rb +30 -0
  23. data/test/unit/inspec_tools/inspec_test.rb +54 -0
  24. data/test/unit/inspec_tools/pdf_test.rb +24 -0
  25. data/test/unit/inspec_tools/summary_test.rb +42 -0
  26. data/test/unit/inspec_tools/xccdf_test.rb +50 -0
  27. data/test/unit/inspec_tools_test.rb +7 -0
  28. data/test/unit/test_helper.rb +5 -0
  29. data/test/unit/utils/inspec_util_test.rb +43 -0
  30. metadata +70 -125
  31. data/lib/data/NIST_Map_02052020_CIS_Controls_Version_7.1_Implementation_Groups_1.2.xlsx +0 -0
  32. data/lib/exceptions/impact_input_error.rb +0 -6
  33. data/lib/exceptions/severity_input_error.rb +0 -6
  34. data/lib/inspec_tools/plugin.rb +0 -15
  35. data/lib/inspec_tools/plugin_cli.rb +0 -278
  36. data/lib/inspec_tools/xlsx_tool.rb +0 -148
  37. data/lib/inspec_tools_plugin.rb +0 -7
  38. data/lib/overrides/false_class.rb +0 -5
  39. data/lib/overrides/nil_class.rb +0 -5
  40. data/lib/overrides/object.rb +0 -5
  41. data/lib/overrides/string.rb +0 -5
  42. data/lib/overrides/true_class.rb +0 -5
@@ -1,6 +0,0 @@
1
- module Utils
2
- class InspecUtil
3
- class ImpactInputError < ::StandardError
4
- end
5
- end
6
- end
@@ -1,6 +0,0 @@
1
- module Utils
2
- class InspecUtil
3
- class SeverityInputError < ::StandardError
4
- end
5
- end
6
- end
@@ -1,15 +0,0 @@
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
@@ -1,278 +0,0 @@
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
@@ -1,148 +0,0 @@
1
- require 'nokogiri'
2
- require 'inspec-objects'
3
- require 'word_wrap'
4
- require 'yaml'
5
- require 'digest'
6
- require 'roo'
7
-
8
- require_relative '../utilities/inspec_util'
9
-
10
- # rubocop:disable Metrics/AbcSize
11
- # rubocop:disable Metrics/PerceivedComplexity
12
- # rubocop:disable Metrics/CyclomaticComplexity
13
-
14
- module InspecTools
15
- # Methods for converting from XLS to various formats
16
- class XLSXTool
17
- CIS_2_NIST_XLSX = Roo::Spreadsheet.open(File.join(File.dirname(__FILE__), '../data/NIST_Map_02052020_CIS_Controls_Version_7.1_Implementation_Groups_1.2.xlsx'))
18
- LATEST_NIST_REV = 'Rev_4'.freeze
19
-
20
- def initialize(xlsx, mapping, name, verbose = false)
21
- @name = name
22
- @xlsx = xlsx
23
- @mapping = mapping
24
- @verbose = verbose
25
- @cis_to_nist = get_cis_to_nist_control_mapping(CIS_2_NIST_XLSX)
26
- end
27
-
28
- def to_ckl
29
- # TODO
30
- end
31
-
32
- def to_xccdf
33
- # TODO
34
- end
35
-
36
- def to_inspec(control_prefix)
37
- @controls = []
38
- @cci_xml = nil
39
- @profile = {}
40
- insert_json_metadata
41
- parse_cis_controls(control_prefix)
42
- @profile['controls'] = @controls
43
- @profile['sha256'] = Digest::SHA256.hexdigest @profile.to_s
44
- @profile
45
- end
46
-
47
- private
48
-
49
- def get_cis_to_nist_control_mapping(spreadsheet)
50
- cis_to_nist = {}
51
- spreadsheet.sheet(3).each do |row|
52
- if row[3].is_a? Numeric
53
- cis_to_nist[row[3].to_s] = row[0]
54
- else
55
- cis_to_nist[row[2].to_s] = row[0] unless (row[2] == '') || row[2].to_i.nil?
56
- end
57
- end
58
- cis_to_nist
59
- end
60
-
61
- def insert_json_metadata
62
- @profile['name'] = @name
63
- @profile['title'] = 'InSpec Profile'
64
- @profile['maintainer'] = 'The Authors'
65
- @profile['copyright'] = 'The Authors'
66
- @profile['copyright_email'] = 'you@example.com'
67
- @profile['license'] = 'Apache-2.0'
68
- @profile['summary'] = 'An InSpec Compliance Profile'
69
- @profile['version'] = '0.1.0'
70
- @profile['supports'] = []
71
- @profile['attributes'] = []
72
- @profile['generator'] = {
73
- 'name': 'inspec_tools',
74
- 'version': ::InspecTools::VERSION
75
- }
76
- end
77
-
78
- def parse_cis_controls(control_prefix)
79
- [1, 2].each do |level|
80
- @xlsx.sheet(level).each_row_streaming do |row|
81
- if row[@mapping['control.id']].nil? || !/^\d+(\.?\d)*$/.match(row[@mapping['control.id']].formatted_value)
82
- next
83
- end
84
-
85
- tag_pos = @mapping['control.tags']
86
- control = {}
87
- control['tags'] = {}
88
- control['id'] = control_prefix + '-' + row[@mapping['control.id']].formatted_value unless cell_empty?(@mapping['control.id']) || cell_empty?(row[@mapping['control.id']])
89
- control['title'] = row[@mapping['control.title']].formatted_value unless cell_empty?(@mapping['control.title']) || cell_empty?(row[@mapping['control.title']])
90
- control['desc'] = ''
91
- control['desc'] = row[@mapping['control.desc']].formatted_value unless cell_empty?(row[@mapping['control.desc']])
92
- control['tags']['rationale'] = row[tag_pos['rationale']].formatted_value unless cell_empty?(row[tag_pos['rationale']])
93
-
94
- control['tags']['severity'] = level == 1 ? 'medium' : 'high'
95
- control['impact'] = Utils::InspecUtil.get_impact(control['tags']['severity'])
96
- control['tags']['ref'] = row[@mapping['control.ref']].formatted_value unless cell_empty?(@mapping['control.ref']) || cell_empty?(row[@mapping['control.ref']])
97
- control['tags']['cis_level'] = level unless level.nil?
98
-
99
- unless cell_empty?(row[tag_pos['cis_controls']])
100
- # cis_control must be extracted from CIS control column via regex
101
- cis_tags_array = row[tag_pos['cis_controls']].formatted_value.scan(/CONTROL:v(\d) (\d+)\.?(\d*)/).flatten
102
- cis_tags = %i(revision section sub_section).zip(cis_tags_array).to_h
103
- control = apply_cis_and_nist_controls(control, cis_tags)
104
- end
105
-
106
- control['tags']['cis_rid'] = row[@mapping['control.id']].formatted_value unless cell_empty?(@mapping['control.id']) || cell_empty?(row[@mapping['control.id']])
107
- control['tags']['check'] = row[tag_pos['check']].formatted_value unless cell_empty?(tag_pos['check']) || cell_empty?(row[tag_pos['check']])
108
- control['tags']['fix'] = row[tag_pos['fix']].formatted_value unless cell_empty?(tag_pos['fix']) || cell_empty?(row[tag_pos['fix']])
109
-
110
- @controls << control
111
- end
112
- end
113
- end
114
-
115
- def cell_empty?(cell)
116
- return cell.empty? if cell.respond_to?(:empty?)
117
-
118
- cell.nil?
119
- end
120
-
121
- def apply_cis_and_nist_controls(control, cis_tags)
122
- control['tags']['cis_controls'], control['tags']['nist'] = [], []
123
-
124
- if cis_tags[:sub_section].nil? || cis_tags[:sub_section].blank?
125
- control['tags']['cis_controls'] << cis_tags[:section]
126
- control['tags']['nist'] << get_nist_control_for_cis(cis_tags[:section])
127
- else
128
- control['tags']['cis_controls'] << "#{cis_tags[:section]}.#{cis_tags[:sub_section]}"
129
- control['tags']['nist'] << get_nist_control_for_cis(cis_tags[:section], cis_tags[:sub_section])
130
- end
131
-
132
- control['tags']['nist'] << LATEST_NIST_REV unless control['tags']['nist'].nil?
133
- control['tags']['cis_controls'] << "Rev_#{cis_tags[:revision]}" unless cis_tags[:revision].nil?
134
-
135
- control
136
- end
137
-
138
- def get_nist_control_for_cis(section, sub_section = nil)
139
- return @cis_to_nist[section] if sub_section.nil?
140
-
141
- @cis_to_nist["#{section}.#{sub_section}"]
142
- end
143
- end
144
- end
145
-
146
- # rubocop:enable Metrics/AbcSize
147
- # rubocop:enable Metrics/PerceivedComplexity
148
- # rubocop:enable Metrics/CyclomaticComplexity