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,148 @@
|
|
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
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'roo'
|
2
|
+
|
3
|
+
module Util
|
4
|
+
class ExtractNistMappings
|
5
|
+
def initialize(file)
|
6
|
+
@file = file
|
7
|
+
@full_excel = []
|
8
|
+
@headers = {}
|
9
|
+
|
10
|
+
open_excel
|
11
|
+
set_working_sheet
|
12
|
+
map_headers
|
13
|
+
retrieve_mappings
|
14
|
+
end
|
15
|
+
|
16
|
+
def open_excel
|
17
|
+
@xlsx = Roo::Excelx.new(@file)
|
18
|
+
end
|
19
|
+
|
20
|
+
def full_excl
|
21
|
+
@full_excel
|
22
|
+
end
|
23
|
+
|
24
|
+
def set_working_sheet
|
25
|
+
@xlsx.default_sheet = 'VER 6.1 Controls'
|
26
|
+
end
|
27
|
+
|
28
|
+
def map_headers
|
29
|
+
@xlsx.row(3).each_with_index { |header, i|
|
30
|
+
@headers[header] = i
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
34
|
+
def retrieve_mappings
|
35
|
+
nist_ver = 4
|
36
|
+
cis_ver = @xlsx.row(2)[4].split(' ')[-1]
|
37
|
+
ctrl_count = 1
|
38
|
+
((@xlsx.first_row + 3)..@xlsx.last_row).each do |row_value|
|
39
|
+
current_row = {}
|
40
|
+
if @xlsx.row(row_value)[@headers['NIST SP 800-53 Control #']].to_s != ''
|
41
|
+
current_row[:nist] = @xlsx.row(row_value)[@headers['NIST SP 800-53 Control #']].to_s
|
42
|
+
else
|
43
|
+
current_row[:nist] = 'Not Mapped'
|
44
|
+
end
|
45
|
+
current_row[:nist_ver] = nist_ver
|
46
|
+
if @xlsx.row(row_value)[@headers['Control']].to_s == ''
|
47
|
+
current_row[:cis] = ctrl_count.to_s
|
48
|
+
ctrl_count += 1
|
49
|
+
else
|
50
|
+
current_row[:cis] = @xlsx.row(row_value)[@headers['Control']].to_s
|
51
|
+
end
|
52
|
+
current_row[:cis_ver] = cis_ver
|
53
|
+
@full_excel << current_row
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'pdf-reader'
|
2
|
+
|
3
|
+
module Util
|
4
|
+
class ExtractPdfText
|
5
|
+
def initialize(pdf)
|
6
|
+
@pdf = pdf
|
7
|
+
@extracted_text = ''
|
8
|
+
read_text
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_reader :extracted_text
|
12
|
+
|
13
|
+
def read_text
|
14
|
+
reader = PDF::Reader.new(@pdf.path)
|
15
|
+
reader.pages.each do |page|
|
16
|
+
@extracted_text += page.text
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,435 @@
|
|
1
|
+
require 'inspec-objects'
|
2
|
+
require 'word_wrap'
|
3
|
+
require 'pp'
|
4
|
+
require 'uri'
|
5
|
+
require 'net/http'
|
6
|
+
require 'fileutils'
|
7
|
+
require 'exceptions/impact_input_error'
|
8
|
+
require 'exceptions/severity_input_error'
|
9
|
+
require 'overrides/false_class'
|
10
|
+
require 'overrides/true_class'
|
11
|
+
require 'overrides/nil_class'
|
12
|
+
require 'overrides/object'
|
13
|
+
require 'overrides/string'
|
14
|
+
|
15
|
+
# rubocop:disable Metrics/ClassLength
|
16
|
+
# rubocop:disable Metrics/AbcSize
|
17
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
18
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
19
|
+
# rubocop:disable Metrics/MethodLength
|
20
|
+
|
21
|
+
module Utils
|
22
|
+
class InspecUtil
|
23
|
+
DATA_NOT_FOUND_MESSAGE = 'N/A'.freeze
|
24
|
+
WIDTH = 80
|
25
|
+
IMPACT_SCORES = {
|
26
|
+
'none' => 0.0,
|
27
|
+
'low' => 0.1,
|
28
|
+
'medium' => 0.4,
|
29
|
+
'high' => 0.7,
|
30
|
+
'critical' => 0.9
|
31
|
+
}.freeze
|
32
|
+
|
33
|
+
def self.parse_data_for_xccdf(json)
|
34
|
+
data = {}
|
35
|
+
|
36
|
+
controls = []
|
37
|
+
if json['profiles'].nil?
|
38
|
+
controls = json['controls']
|
39
|
+
elsif json['profiles'].length == 1
|
40
|
+
controls = json['profiles'].last['controls']
|
41
|
+
else
|
42
|
+
json['profiles'].each do |profile|
|
43
|
+
controls.concat(profile['controls'])
|
44
|
+
end
|
45
|
+
end
|
46
|
+
c_data = {}
|
47
|
+
|
48
|
+
controls.each do |control|
|
49
|
+
c_id = control['id'].to_sym
|
50
|
+
c_data[c_id] = {}
|
51
|
+
c_data[c_id]['id'] = control['id'] || DATA_NOT_FOUND_MESSAGE
|
52
|
+
c_data[c_id]['title'] = control['title'] || DATA_NOT_FOUND_MESSAGE
|
53
|
+
c_data[c_id]['desc'] = control['desc'] || DATA_NOT_FOUND_MESSAGE
|
54
|
+
c_data[c_id]['severity'] = control['tags']['severity'] || DATA_NOT_FOUND_MESSAGE
|
55
|
+
c_data[c_id]['gid'] = control['tags']['gid'] || DATA_NOT_FOUND_MESSAGE
|
56
|
+
c_data[c_id]['gtitle'] = control['tags']['gtitle'] || DATA_NOT_FOUND_MESSAGE
|
57
|
+
c_data[c_id]['gdescription'] = control['tags']['gdescription'] || DATA_NOT_FOUND_MESSAGE
|
58
|
+
c_data[c_id]['rid'] = control['tags']['rid'] || DATA_NOT_FOUND_MESSAGE
|
59
|
+
c_data[c_id]['rversion'] = control['tags']['rversion'] || DATA_NOT_FOUND_MESSAGE
|
60
|
+
c_data[c_id]['rweight'] = control['tags']['rweight'] || DATA_NOT_FOUND_MESSAGE
|
61
|
+
c_data[c_id]['stig_id'] = control['tags']['stig_id'] || DATA_NOT_FOUND_MESSAGE
|
62
|
+
c_data[c_id]['cci'] = control['tags']['cci'] || DATA_NOT_FOUND_MESSAGE
|
63
|
+
c_data[c_id]['nist'] = control['tags']['nist'] || ['unmapped']
|
64
|
+
c_data[c_id]['check'] = control['tags']['check'] || DATA_NOT_FOUND_MESSAGE
|
65
|
+
c_data[c_id]['checkref'] = control['tags']['checkref'] || DATA_NOT_FOUND_MESSAGE
|
66
|
+
c_data[c_id]['fix'] = control['tags']['fix'] || DATA_NOT_FOUND_MESSAGE
|
67
|
+
c_data[c_id]['fixref'] = control['tags']['fixref'] || DATA_NOT_FOUND_MESSAGE
|
68
|
+
c_data[c_id]['fix_id'] = control['tags']['fix_id'] || DATA_NOT_FOUND_MESSAGE
|
69
|
+
c_data[c_id]['rationale'] = control['tags']['rationale'] || DATA_NOT_FOUND_MESSAGE
|
70
|
+
c_data[c_id]['cis_family'] = control['tags']['cis_family'] || DATA_NOT_FOUND_MESSAGE
|
71
|
+
c_data[c_id]['cis_rid'] = control['tags']['cis_rid'] || DATA_NOT_FOUND_MESSAGE
|
72
|
+
c_data[c_id]['cis_level'] = control['tags']['cis_level'] || DATA_NOT_FOUND_MESSAGE
|
73
|
+
c_data[c_id]['impact'] = control['impact'].to_s || DATA_NOT_FOUND_MESSAGE
|
74
|
+
c_data[c_id]['code'] = control['code'].to_s || DATA_NOT_FOUND_MESSAGE
|
75
|
+
end
|
76
|
+
|
77
|
+
data['controls'] = c_data.values
|
78
|
+
data['status'] = 'success'
|
79
|
+
data
|
80
|
+
end
|
81
|
+
|
82
|
+
def self.parse_data_for_ckl(json)
|
83
|
+
data = {}
|
84
|
+
|
85
|
+
# Parse for inspec profile results json
|
86
|
+
json['profiles'].each do |profile|
|
87
|
+
profile['controls'].each do |control|
|
88
|
+
c_id = control['id'].to_sym
|
89
|
+
data[c_id] = {}
|
90
|
+
|
91
|
+
data[c_id][:vuln_num] = control['id'] unless control['id'].nil?
|
92
|
+
data[c_id][:rule_title] = control['title'] unless control['title'].nil?
|
93
|
+
data[c_id][:vuln_discuss] = control['desc'] unless control['desc'].nil?
|
94
|
+
|
95
|
+
unless control['tags'].nil?
|
96
|
+
data[c_id][:severity] = control['tags']['severity'] unless control['tags']['severity'].nil?
|
97
|
+
data[c_id][:gid] = control['tags']['gid'] unless control['tags']['gid'].nil?
|
98
|
+
data[c_id][:group_title] = control['tags']['gtitle'] unless control['tags']['gtitle'].nil?
|
99
|
+
data[c_id][:rule_id] = control['tags']['rid'] unless control['tags']['rid'].nil?
|
100
|
+
data[c_id][:rule_ver] = control['tags']['stig_id'] unless control['tags']['stig_id'].nil?
|
101
|
+
data[c_id][:cci_ref] = control['tags']['cci'] unless control['tags']['cci'].nil?
|
102
|
+
data[c_id][:nist] = control['tags']['nist'].join(' ') unless control['tags']['nist'].nil?
|
103
|
+
end
|
104
|
+
|
105
|
+
if control['descriptions'].respond_to?(:find)
|
106
|
+
data[c_id][:check_content] = control['descriptions'].find { |c| c['label'] == 'fix' }&.dig('data')
|
107
|
+
data[c_id][:fix_text] = control['descriptions'].find { |c| c['label'] == 'check' }&.dig('data')
|
108
|
+
end
|
109
|
+
|
110
|
+
data[c_id][:impact] = control['impact'].to_s unless control['impact'].nil?
|
111
|
+
data[c_id][:profile_name] = profile['name'].to_s unless profile['name'].nil?
|
112
|
+
data[c_id][:profile_shasum] = profile['sha256'].to_s unless profile['sha256'].nil?
|
113
|
+
|
114
|
+
data[c_id][:status] = []
|
115
|
+
data[c_id][:message] = []
|
116
|
+
|
117
|
+
if control.key?('results')
|
118
|
+
control['results'].each do |result|
|
119
|
+
if !result['backtrace'].nil?
|
120
|
+
result['status'] = 'error'
|
121
|
+
end
|
122
|
+
data[c_id][:status].push(result['status'])
|
123
|
+
data[c_id][:message].push("SKIPPED -- Test: #{result['code_desc']}\nMessage: #{result['skip_message']}\n") if result['status'] == 'skipped'
|
124
|
+
data[c_id][:message].push("FAILED -- Test: #{result['code_desc']}\nMessage: #{result['message']}\n") if result['status'] == 'failed'
|
125
|
+
data[c_id][:message].push("PASS -- #{result['code_desc']}\n") if result['status'] == 'passed'
|
126
|
+
data[c_id][:message].push("PROFILE_ERROR -- Test: #{result['code_desc']}\nMessage: #{result['backtrace']}\n") if result['status'] == 'error'
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
if data[c_id][:impact].to_f.zero?
|
131
|
+
data[c_id][:message].unshift("NOT_APPLICABLE -- Description: #{control['desc']}\n\n")
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
data
|
136
|
+
end
|
137
|
+
|
138
|
+
def self.get_platform(json)
|
139
|
+
json['profiles'].find { |profile| !profile[:platform].nil? }
|
140
|
+
end
|
141
|
+
|
142
|
+
def self.to_dotted_hash(hash, recursive_key = '')
|
143
|
+
hash.each_with_object({}) do |(k, v), ret|
|
144
|
+
key = recursive_key + k.to_s
|
145
|
+
if v.is_a? Hash
|
146
|
+
ret.merge! to_dotted_hash(v, key + '.')
|
147
|
+
else
|
148
|
+
ret[key] = v
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def self.control_status(control, for_summary = false)
|
154
|
+
status_list = control[:status].uniq
|
155
|
+
if control[:impact].to_f.zero?
|
156
|
+
'Not_Applicable'
|
157
|
+
elsif status_list.include?('failed')
|
158
|
+
'Open'
|
159
|
+
elsif status_list.include?('passed')
|
160
|
+
'NotAFinding'
|
161
|
+
elsif status_list.include?('error') && for_summary
|
162
|
+
'Profile_Error'
|
163
|
+
else
|
164
|
+
# profile skipped or profile error
|
165
|
+
'Not_Reviewed'
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def self.control_finding_details(control, control_clk_status)
|
170
|
+
result = "One or more of the automated tests failed or was inconclusive for the control \n\n #{control[:message].sort.join}" if control_clk_status == 'Open'
|
171
|
+
result = "All Automated tests passed for the control \n\n #{control[:message].join}" if control_clk_status == 'NotAFinding'
|
172
|
+
result = "Automated test skipped due to known accepted condition in the control : \n\n#{control[:message].join}" if control_clk_status == 'Not_Reviewed'
|
173
|
+
result = "Justification: \n #{control[:message].join}" if control_clk_status == 'Not_Applicable'
|
174
|
+
result = 'No test available or some test errors occurred for this control' if control_clk_status == 'Profile_Error'
|
175
|
+
result
|
176
|
+
end
|
177
|
+
|
178
|
+
# @!method get_impact(severity)
|
179
|
+
# Takes in the STIG severity tag and converts it to the InSpec #{impact}
|
180
|
+
# control tag.
|
181
|
+
# At the moment the mapping is static, so that:
|
182
|
+
# high => 0.7
|
183
|
+
# medium => 0.5
|
184
|
+
# low => 0.3
|
185
|
+
# @param severity [String] the string value you want to map to an InSpec
|
186
|
+
# 'impact' level.
|
187
|
+
#
|
188
|
+
# @return impact [Float] the impact level level mapped to the XCCDF severity
|
189
|
+
# mapped to a float between 0.0 - 1.0.
|
190
|
+
#
|
191
|
+
# @todo Allow for the user to pass in a hash for the desired mapping of text
|
192
|
+
# values to numbers or to override our hard coded values.
|
193
|
+
#
|
194
|
+
def self.get_impact(severity, use_cvss_terms: true)
|
195
|
+
return float_to_impact(severity, use_cvss_terms) if severity.is_a?(Float)
|
196
|
+
|
197
|
+
return string_to_impact(severity, use_cvss_terms) if severity.is_a?(String)
|
198
|
+
|
199
|
+
raise SeverityInputError, "'#{severity}' is not a valid severity value. It should be a Float between 0.0 and " \
|
200
|
+
'1.0 or one of the approved keywords.'
|
201
|
+
end
|
202
|
+
|
203
|
+
private_class_method def self.float_to_impact(severity, use_cvss_terms)
|
204
|
+
unless severity.between?(0, 1)
|
205
|
+
raise SeverityInputError, "'#{severity}' is not a valid severity value. It should be a Float between 0.0 and " \
|
206
|
+
'1.0 or one of the approved keywords.'
|
207
|
+
end
|
208
|
+
|
209
|
+
if severity <= 0.01
|
210
|
+
0.0 # Informative
|
211
|
+
elsif severity < 0.4
|
212
|
+
0.3 # Low Impact
|
213
|
+
elsif severity < 0.7
|
214
|
+
0.5 # Medium Impact
|
215
|
+
elsif severity < 0.9 || use_cvss_terms
|
216
|
+
0.7 # High Impact
|
217
|
+
else
|
218
|
+
1.0 # Critical Controls
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
private_class_method def self.string_to_impact(severity, use_cvss_terms)
|
223
|
+
if /none|na|n\/a|not[_|(\s*)]?applicable/i.match?(severity)
|
224
|
+
impact = 0.0 # Informative
|
225
|
+
elsif /low|cat(egory)?\s*(iii|3)/i.match?(severity)
|
226
|
+
impact = 0.3 # Low Impact
|
227
|
+
elsif /med(ium)?|cat(egory)?\s*(ii|2)/i.match?(severity)
|
228
|
+
impact = 0.5 # Medium Impact
|
229
|
+
elsif /high|cat(egory)?\s*(i|1)/i.match?(severity)
|
230
|
+
impact = 0.7 # High Impact
|
231
|
+
elsif /crit(ical)?|severe/i.match?(severity)
|
232
|
+
impact = 1.0 # Critical Controls
|
233
|
+
else
|
234
|
+
raise SeverityInputError, "'#{severity}' is not a valid severity value. It should be a Float between 0.0 and " \
|
235
|
+
'1.0 or one of the approved keywords.'
|
236
|
+
end
|
237
|
+
|
238
|
+
impact == 1.0 && use_cvss_terms ? 0.7 : impact
|
239
|
+
end
|
240
|
+
|
241
|
+
def self.get_impact_string(impact, use_cvss_terms: true)
|
242
|
+
return if impact.nil?
|
243
|
+
|
244
|
+
value = impact.to_f
|
245
|
+
unless value.between?(0, 1)
|
246
|
+
raise ImpactInputError, "'#{value}' is not a valid impact score. Valid impact scores: [0.0 - 1.0]."
|
247
|
+
end
|
248
|
+
|
249
|
+
IMPACT_SCORES.reverse_each do |name, impact_score|
|
250
|
+
if name == 'critical' && value >= impact_score && use_cvss_terms
|
251
|
+
return 'high'
|
252
|
+
elsif value >= impact_score
|
253
|
+
return name
|
254
|
+
else
|
255
|
+
next
|
256
|
+
end
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
def self.unpack_inspec_json(directory, inspec_json, separated, output_format)
|
261
|
+
if directory == 'id'
|
262
|
+
directory = inspec_json['name']
|
263
|
+
end
|
264
|
+
controls = generate_controls(inspec_json)
|
265
|
+
unpack_profile(directory || 'profile', controls, separated, output_format || 'json')
|
266
|
+
create_inspec_yml(directory || 'profile', inspec_json)
|
267
|
+
create_license(directory || 'profile', inspec_json)
|
268
|
+
create_readme_md(directory || 'profile', inspec_json)
|
269
|
+
end
|
270
|
+
|
271
|
+
private_class_method def self.wrap(str, width = WIDTH)
|
272
|
+
str.gsub!("desc \"\n ", 'desc "')
|
273
|
+
str.gsub!(/\\r/, "\n")
|
274
|
+
str.gsub!(/\\n/, "\n")
|
275
|
+
|
276
|
+
WordWrap.ww(str.to_s, width)
|
277
|
+
end
|
278
|
+
|
279
|
+
private_class_method def self.generate_controls(inspec_json)
|
280
|
+
controls = []
|
281
|
+
inspec_json['controls'].each do |json_control|
|
282
|
+
control = ::Inspec::Object::Control.new
|
283
|
+
if (defined? control.desc).nil?
|
284
|
+
control.descriptions[:default] = json_control['desc']
|
285
|
+
control.descriptions[:rationale] = json_control['tags']['rationale']
|
286
|
+
control.descriptions[:check] = json_control['tags']['check']
|
287
|
+
control.descriptions[:fix] = json_control['tags']['fix']
|
288
|
+
else
|
289
|
+
control.desc = json_control['desc']
|
290
|
+
end
|
291
|
+
control.id = json_control['id']
|
292
|
+
control.title = json_control['title']
|
293
|
+
control.impact = get_impact(json_control['impact'])
|
294
|
+
|
295
|
+
# json_control['tags'].each do |tag|
|
296
|
+
# control.add_tag(Inspec::Object::Tag.new(tag.key, tag.value)
|
297
|
+
# end
|
298
|
+
|
299
|
+
control.add_tag(::Inspec::Object::Tag.new('severity', json_control['tags']['severity']))
|
300
|
+
control.add_tag(::Inspec::Object::Tag.new('gtitle', json_control['tags']['gtitle']))
|
301
|
+
control.add_tag(::Inspec::Object::Tag.new('satisfies', json_control['tags']['satisfies'])) if json_control['tags']['satisfies']
|
302
|
+
control.add_tag(::Inspec::Object::Tag.new('gid', json_control['tags']['gid']))
|
303
|
+
control.add_tag(::Inspec::Object::Tag.new('rid', json_control['tags']['rid']))
|
304
|
+
control.add_tag(::Inspec::Object::Tag.new('stig_id', json_control['tags']['stig_id']))
|
305
|
+
control.add_tag(::Inspec::Object::Tag.new('fix_id', json_control['tags']['fix_id']))
|
306
|
+
control.add_tag(::Inspec::Object::Tag.new('cci', json_control['tags']['cci']))
|
307
|
+
control.add_tag(::Inspec::Object::Tag.new('nist', json_control['tags']['nist']))
|
308
|
+
control.add_tag(::Inspec::Object::Tag.new('cis_level', json_control['tags']['cis_level'])) unless json_control['tags']['cis_level'].blank?
|
309
|
+
control.add_tag(::Inspec::Object::Tag.new('cis_controls', json_control['tags']['cis_controls'])) unless json_control['tags']['cis_controls'].blank?
|
310
|
+
control.add_tag(::Inspec::Object::Tag.new('cis_rid', json_control['tags']['cis_rid'])) unless json_control['tags']['cis_rid'].blank?
|
311
|
+
control.add_tag(::Inspec::Object::Tag.new('ref', json_control['tags']['ref'])) unless json_control['tags']['ref'].blank?
|
312
|
+
control.add_tag(::Inspec::Object::Tag.new('false_negatives', json_control['tags']['false_negatives'])) unless json_control['tags']['false_positives'].blank?
|
313
|
+
control.add_tag(::Inspec::Object::Tag.new('false_positives', json_control['tags']['false_positives'])) unless json_control['tags']['false_positives'].blank?
|
314
|
+
control.add_tag(::Inspec::Object::Tag.new('documentable', json_control['tags']['documentable'])) unless json_control['tags']['documentable'].blank?
|
315
|
+
control.add_tag(::Inspec::Object::Tag.new('mitigations', json_control['tags']['mitigations'])) unless json_control['tags']['mitigations'].blank?
|
316
|
+
control.add_tag(::Inspec::Object::Tag.new('severity_override_guidance', json_control['tags']['severity_override_guidance'])) unless json_control['tags']['severity_override_guidance'].blank?
|
317
|
+
control.add_tag(::Inspec::Object::Tag.new('potential_impacts', json_control['tags']['potential_impacts'])) unless json_control['tags']['potential_impacts'].blank?
|
318
|
+
control.add_tag(::Inspec::Object::Tag.new('third_party_tools', json_control['tags']['third_party_tools'])) unless json_control['tags']['third_party_tools'].blank?
|
319
|
+
control.add_tag(::Inspec::Object::Tag.new('mitigation_controls', json_control['tags']['mitigation_controls'])) unless json_control['tags']['mitigation_controls'].blank?
|
320
|
+
control.add_tag(::Inspec::Object::Tag.new('responsibility', json_control['tags']['responsibility'])) unless json_control['tags']['responsibility'].blank?
|
321
|
+
control.add_tag(::Inspec::Object::Tag.new('ia_controls', json_control['tags']['ia_controls'])) unless json_control['tags']['ia_controls'].blank?
|
322
|
+
|
323
|
+
controls << control
|
324
|
+
end
|
325
|
+
controls
|
326
|
+
end
|
327
|
+
|
328
|
+
# @!method print_benchmark_info(info)
|
329
|
+
# writes benchmark info to profile inspec.yml file
|
330
|
+
#
|
331
|
+
private_class_method def self.create_inspec_yml(directory, inspec_json)
|
332
|
+
benchmark_info =
|
333
|
+
"name: #{inspec_json['name']}\n" \
|
334
|
+
"title: #{inspec_json['title']}\n" \
|
335
|
+
"maintainer: #{inspec_json['maintainer']}\n" \
|
336
|
+
"copyright: #{inspec_json['copyright']}\n" \
|
337
|
+
"copyright_email: #{inspec_json['copyright_email']}\n" \
|
338
|
+
"license: #{inspec_json['license']}\n" \
|
339
|
+
"summary: #{inspec_json['summary']}\n" \
|
340
|
+
"version: #{inspec_json['version']}\n"
|
341
|
+
|
342
|
+
myfile = File.new("#{directory}/inspec.yml", 'w')
|
343
|
+
myfile.puts benchmark_info
|
344
|
+
end
|
345
|
+
|
346
|
+
private_class_method def self.create_license(directory, inspec_json)
|
347
|
+
license_content = ''
|
348
|
+
if !inspec_json['license'].nil?
|
349
|
+
begin
|
350
|
+
response = Net::HTTP.get_response(URI(inspec_json['license']))
|
351
|
+
if response.code == '200'
|
352
|
+
license_content = response.body
|
353
|
+
else
|
354
|
+
license_content = inspec_json['license']
|
355
|
+
end
|
356
|
+
rescue StandardError => _e
|
357
|
+
license_content = inspec_json['license']
|
358
|
+
end
|
359
|
+
end
|
360
|
+
|
361
|
+
myfile = File.new("#{directory}/LICENSE", 'w')
|
362
|
+
myfile.puts license_content
|
363
|
+
end
|
364
|
+
|
365
|
+
private_class_method def self.create_readme_md(directory, inspec_json)
|
366
|
+
readme_contents =
|
367
|
+
"\# #{inspec_json['title']}\n" \
|
368
|
+
"#{inspec_json['summary']}\n" \
|
369
|
+
"---\n" \
|
370
|
+
"Name: #{inspec_json['name']}\n" \
|
371
|
+
"Author: #{inspec_json['maintainer']}\n" \
|
372
|
+
"Status: #{inspec_json['status']}\n" \
|
373
|
+
"Copyright: #{inspec_json['copyright']}\n" \
|
374
|
+
"Copyright Email: #{inspec_json['copyright_email']}\n" \
|
375
|
+
"Version: #{inspec_json['version']}\n" \
|
376
|
+
"#{inspec_json['plaintext']}\n" \
|
377
|
+
"Reference: #{inspec_json['reference_href']}\n" \
|
378
|
+
"Reference by: #{inspec_json['reference_publisher']}\n" \
|
379
|
+
"Reference source: #{inspec_json['reference_source']}\n"
|
380
|
+
|
381
|
+
myfile = File.new("#{directory}/README.md", 'w')
|
382
|
+
myfile.puts readme_contents
|
383
|
+
end
|
384
|
+
|
385
|
+
private_class_method def self.unpack_profile(directory, controls, separated, output_format)
|
386
|
+
FileUtils.rm_rf(directory) if Dir.exist?(directory)
|
387
|
+
Dir.mkdir directory unless Dir.exist?(directory)
|
388
|
+
Dir.mkdir "#{directory}/controls" unless Dir.exist?("#{directory}/controls")
|
389
|
+
Dir.mkdir "#{directory}/libraries" unless Dir.exist?("#{directory}/libraries")
|
390
|
+
if separated
|
391
|
+
if output_format == 'ruby'
|
392
|
+
controls.each do |control|
|
393
|
+
file_name = control.id.to_s
|
394
|
+
myfile = File.new("#{directory}/controls/#{file_name}.rb", 'w')
|
395
|
+
myfile.puts "# encoding: UTF-8\n\n"
|
396
|
+
myfile.puts wrap(control.to_ruby.gsub('"', "\'"), WIDTH) + "\n"
|
397
|
+
myfile.close
|
398
|
+
end
|
399
|
+
else
|
400
|
+
controls.each do |control|
|
401
|
+
file_name = control.id.to_s
|
402
|
+
myfile = File.new("#{directory}/controls/#{file_name}.rb", 'w')
|
403
|
+
PP.pp(control.to_hash, myfile)
|
404
|
+
myfile.close
|
405
|
+
end
|
406
|
+
end
|
407
|
+
else
|
408
|
+
myfile = File.new("#{directory}/controls/controls.rb", 'w')
|
409
|
+
if output_format == 'ruby'
|
410
|
+
controls.each do |control|
|
411
|
+
myfile.puts "# encoding: UTF-8\n\n"
|
412
|
+
myfile.puts wrap(control.to_ruby.gsub('"', "\'"), WIDTH) + "\n"
|
413
|
+
end
|
414
|
+
else
|
415
|
+
controls.each do |control|
|
416
|
+
if (defined? control.desc).nil?
|
417
|
+
control.descriptions[:default].strip!
|
418
|
+
else
|
419
|
+
control.desc.strip!
|
420
|
+
end
|
421
|
+
|
422
|
+
PP.pp(control.to_hash, myfile)
|
423
|
+
end
|
424
|
+
end
|
425
|
+
myfile.close
|
426
|
+
end
|
427
|
+
end
|
428
|
+
end
|
429
|
+
end
|
430
|
+
|
431
|
+
# rubocop:enable Metrics/ClassLength
|
432
|
+
# rubocop:enable Metrics/AbcSize
|
433
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
434
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
435
|
+
# rubocop:enable Metrics/MethodLength
|