inspec_tools 2.0.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.md +15 -0
  3. data/README.md +373 -0
  4. data/Rakefile +96 -0
  5. data/exe/inspec_tools +14 -0
  6. data/lib/data/README.TXT +25 -0
  7. data/lib/data/U_CCI_List.xml +38403 -0
  8. data/lib/data/attributes.yml +23 -0
  9. data/lib/data/cci2html.xsl +136 -0
  10. data/lib/data/cis_to_nist_critical_controls +0 -0
  11. data/lib/data/cis_to_nist_mapping +0 -0
  12. data/lib/data/mapping.yml +17 -0
  13. data/lib/data/rubocop.yml +4 -0
  14. data/lib/data/stig.csv +1 -0
  15. data/lib/data/threshold.yaml +83 -0
  16. data/lib/exceptions/impact_input_error.rb +6 -0
  17. data/lib/exceptions/severity_input_error.rb +6 -0
  18. data/lib/happy_mapper_tools/benchmark.rb +161 -0
  19. data/lib/happy_mapper_tools/cci_attributes.rb +66 -0
  20. data/lib/happy_mapper_tools/stig_attributes.rb +216 -0
  21. data/lib/happy_mapper_tools/stig_checklist.rb +99 -0
  22. data/lib/inspec_tools.rb +17 -0
  23. data/lib/inspec_tools/ckl.rb +20 -0
  24. data/lib/inspec_tools/cli.rb +31 -0
  25. data/lib/inspec_tools/csv.rb +101 -0
  26. data/lib/inspec_tools/help.rb +9 -0
  27. data/lib/inspec_tools/help/compliance.md +7 -0
  28. data/lib/inspec_tools/help/csv2inspec.md +5 -0
  29. data/lib/inspec_tools/help/inspec2ckl.md +5 -0
  30. data/lib/inspec_tools/help/inspec2csv.md +5 -0
  31. data/lib/inspec_tools/help/inspec2xccdf.md +5 -0
  32. data/lib/inspec_tools/help/pdf2inspec.md +6 -0
  33. data/lib/inspec_tools/help/summary.md +5 -0
  34. data/lib/inspec_tools/help/xccdf2inspec.md +5 -0
  35. data/lib/inspec_tools/inspec.rb +331 -0
  36. data/lib/inspec_tools/pdf.rb +125 -0
  37. data/lib/inspec_tools/plugin.rb +15 -0
  38. data/lib/inspec_tools/plugin_cli.rb +275 -0
  39. data/lib/inspec_tools/summary.rb +126 -0
  40. data/lib/inspec_tools/version.rb +8 -0
  41. data/lib/inspec_tools/xccdf.rb +156 -0
  42. data/lib/inspec_tools/xlsx_tool.rb +135 -0
  43. data/lib/inspec_tools_plugin.rb +7 -0
  44. data/lib/overrides/false_class.rb +5 -0
  45. data/lib/overrides/nil_class.rb +5 -0
  46. data/lib/overrides/object.rb +5 -0
  47. data/lib/overrides/string.rb +5 -0
  48. data/lib/overrides/true_class.rb +5 -0
  49. data/lib/utilities/cis_to_nist.rb +11 -0
  50. data/lib/utilities/csv_util.rb +14 -0
  51. data/lib/utilities/extract_pdf_text.rb +20 -0
  52. data/lib/utilities/inspec_util.rb +441 -0
  53. data/lib/utilities/parser.rb +373 -0
  54. data/lib/utilities/text_cleaner.rb +69 -0
  55. metadata +359 -0
@@ -0,0 +1,8 @@
1
+ require 'git-version-bump'
2
+
3
+ module InspecTools
4
+ # Enable lite-tags (2nd parameter to git-version-bump version command)
5
+ # Lite tags are tags that are used by GitHub releases that do not contain
6
+ # annotations
7
+ VERSION = GVB.version(false, true)
8
+ end
@@ -0,0 +1,156 @@
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(/(&lt;|<)#{tag}(&gt;|>)/, "$#{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']['security_override_guidance'] = group.rule.description.security_override_guidance if group.rule.description.security_override_guidance != ''
144
+ control['tags']['potential_impacts'] = group.rule.description.potential_impacts if group.rule.description.potential_impacts != ''
145
+ control['tags']['third_party_tools'] = group.rule.description.third_party_tools if group.rule.description.third_party_tools != ''
146
+ control['tags']['mitigation_controls'] = group.rule.description.mitigation_controls if group.rule.description.mitigation_controls != ''
147
+ control['tags']['responsibility'] = group.rule.description.responsibility if group.rule.description.responsibility != ''
148
+ control['tags']['ia_controls'] = group.rule.description.ia_controls if group.rule.description.ia_controls != ''
149
+ control['tags']['check'] = group.rule.check.content
150
+ control['tags']['fix'] = group.rule.fixtext
151
+ @controls << control
152
+ end
153
+ @profile['controls'] = @controls
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,135 @@
1
+ require 'nokogiri'
2
+ require 'inspec-objects'
3
+ require 'word_wrap'
4
+ require 'yaml'
5
+ require 'digest'
6
+
7
+ require_relative '../utilities/inspec_util'
8
+ require_relative '../utilities/cis_to_nist'
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
+ LATEST_NIST_REV = 'Rev_4'.freeze
18
+
19
+ def initialize(xlsx, mapping, name, verbose = false)
20
+ @name = name
21
+ @xlsx = xlsx
22
+ @mapping = mapping
23
+ @verbose = verbose
24
+ @cis_to_nist = Utils::CisToNist.get_mapping('cis_to_nist_mapping')
25
+ end
26
+
27
+ def to_ckl
28
+ # TODO
29
+ end
30
+
31
+ def to_xccdf
32
+ # TODO
33
+ end
34
+
35
+ def to_inspec(control_prefix)
36
+ @controls = []
37
+ @cci_xml = nil
38
+ @profile = {}
39
+ insert_json_metadata
40
+ parse_cis_controls(control_prefix)
41
+ @profile['controls'] = @controls
42
+ @profile['sha256'] = Digest::SHA256.hexdigest @profile.to_s
43
+ @profile
44
+ end
45
+
46
+ private
47
+
48
+ def insert_json_metadata
49
+ @profile['name'] = @name
50
+ @profile['title'] = 'InSpec Profile'
51
+ @profile['maintainer'] = 'The Authors'
52
+ @profile['copyright'] = 'The Authors'
53
+ @profile['copyright_email'] = 'you@example.com'
54
+ @profile['license'] = 'Apache-2.0'
55
+ @profile['summary'] = 'An InSpec Compliance Profile'
56
+ @profile['version'] = '0.1.0'
57
+ @profile['supports'] = []
58
+ @profile['attributes'] = []
59
+ @profile['generator'] = {
60
+ 'name': 'inspec_tools',
61
+ 'version': ::InspecTools::VERSION
62
+ }
63
+ end
64
+
65
+ def parse_cis_controls(control_prefix)
66
+ [1, 2].each do |level|
67
+ @xlsx.sheet(level).each_row_streaming do |row|
68
+ if row[@mapping['control.id']].nil? || !/^\d+(\.?\d)*$/.match(row[@mapping['control.id']].formatted_value)
69
+ next
70
+ end
71
+
72
+ tag_pos = @mapping['control.tags']
73
+ control = {}
74
+ control['tags'] = {}
75
+ control['id'] = control_prefix + '-' + row[@mapping['control.id']].formatted_value unless cell_empty?(@mapping['control.id']) || cell_empty?(row[@mapping['control.id']])
76
+ control['title'] = row[@mapping['control.title']].formatted_value unless cell_empty?(@mapping['control.title']) || cell_empty?(row[@mapping['control.title']])
77
+ control['desc'] = ''
78
+ control['desc'] = row[@mapping['control.desc']].formatted_value unless cell_empty?(row[@mapping['control.desc']])
79
+ control['tags']['rationale'] = row[tag_pos['rationale']].formatted_value unless cell_empty?(row[tag_pos['rationale']])
80
+
81
+ control['tags']['severity'] = level == 1 ? 'medium' : 'high'
82
+ control['impact'] = Utils::InspecUtil.get_impact(control['tags']['severity'])
83
+ control['tags']['ref'] = row[@mapping['control.ref']].formatted_value unless cell_empty?(@mapping['control.ref']) || cell_empty?(row[@mapping['control.ref']])
84
+ control['tags']['cis_level'] = level unless level.nil?
85
+
86
+ unless cell_empty?(row[tag_pos['cis_controls']])
87
+ # cis_control must be extracted from CIS control column via regex
88
+ cis_tags_array = row[tag_pos['cis_controls']].formatted_value.scan(/CONTROL:v(\d) (\d+)\.?(\d*)/).flatten
89
+ cis_tags = %i(revision section sub_section).zip(cis_tags_array).to_h
90
+ control = apply_cis_and_nist_controls(control, cis_tags)
91
+ end
92
+
93
+ control['tags']['cis_rid'] = row[@mapping['control.id']].formatted_value unless cell_empty?(@mapping['control.id']) || cell_empty?(row[@mapping['control.id']])
94
+ control['tags']['check'] = row[tag_pos['check']].formatted_value unless cell_empty?(tag_pos['check']) || cell_empty?(row[tag_pos['check']])
95
+ control['tags']['fix'] = row[tag_pos['fix']].formatted_value unless cell_empty?(tag_pos['fix']) || cell_empty?(row[tag_pos['fix']])
96
+
97
+ @controls << control
98
+ end
99
+ end
100
+ end
101
+
102
+ def cell_empty?(cell)
103
+ return cell.empty? if cell.respond_to?(:empty?)
104
+
105
+ cell.nil?
106
+ end
107
+
108
+ def apply_cis_and_nist_controls(control, cis_tags)
109
+ control['tags']['cis_controls'], control['tags']['nist'] = [], []
110
+
111
+ if cis_tags[:sub_section].nil? || cis_tags[:sub_section].blank?
112
+ control['tags']['cis_controls'] << cis_tags[:section]
113
+ control['tags']['nist'] << get_nist_control_for_cis(cis_tags[:section])
114
+ else
115
+ control['tags']['cis_controls'] << "#{cis_tags[:section]}.#{cis_tags[:sub_section]}"
116
+ control['tags']['nist'] << get_nist_control_for_cis(cis_tags[:section], cis_tags[:sub_section])
117
+ end
118
+
119
+ control['tags']['nist'] << LATEST_NIST_REV unless control['tags']['nist'].nil?
120
+ control['tags']['cis_controls'] << "Rev_#{cis_tags[:revision]}" unless cis_tags[:revision].nil?
121
+
122
+ control
123
+ end
124
+
125
+ def get_nist_control_for_cis(section, sub_section = nil)
126
+ return @cis_to_nist[section] if sub_section.nil?
127
+
128
+ @cis_to_nist["#{section}.#{sub_section}"]
129
+ end
130
+ end
131
+ end
132
+
133
+ # rubocop:enable Metrics/AbcSize
134
+ # rubocop:enable Metrics/PerceivedComplexity
135
+ # rubocop:enable Metrics/CyclomaticComplexity
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ libdir = File.dirname(__FILE__)
4
+ $LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir)
5
+
6
+ require 'inspec_tools/version'
7
+ require 'inspec_tools/plugin'
@@ -0,0 +1,5 @@
1
+ class FalseClass
2
+ def blank?
3
+ true
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class NilClass
2
+ def blank?
3
+ true
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class Object
2
+ def blank?
3
+ respond_to?(:empty?) ? empty? : !self
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class String
2
+ def blank?
3
+ self.strip.empty?
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class TrueClass
2
+ def blank?
3
+ false
4
+ end
5
+ end
@@ -0,0 +1,11 @@
1
+ module Utils
2
+ class CisToNist
3
+ def self.get_mapping(mapping_file)
4
+ path = File.expand_path(File.join(File.expand_path(__dir__), '..', 'data', mapping_file))
5
+ raise "CIS to NIST control mapping does not exist at #{path}. Has it been generated?" unless File.exist?(path)
6
+
7
+ mapping = File.open(path)
8
+ Marshal.load(mapping)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,14 @@
1
+ require 'csv'
2
+
3
+ module Utils
4
+ class CSVUtil
5
+ def self.unpack_csv(csv_string, file)
6
+ csv = CSV.parse(csv_string)
7
+ CSV.open(file, 'wb') do |csv_file|
8
+ csv.each do |line|
9
+ csv_file << line
10
+ end
11
+ end
12
+ end
13
+ end
14
+ 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,441 @@
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
+ require 'rubocop'
15
+
16
+ # rubocop:disable Metrics/ClassLength
17
+ # rubocop:disable Metrics/AbcSize
18
+ # rubocop:disable Metrics/PerceivedComplexity
19
+ # rubocop:disable Metrics/CyclomaticComplexity
20
+ # rubocop:disable Metrics/MethodLength
21
+
22
+ module Utils
23
+ class InspecUtil
24
+ DATA_NOT_FOUND_MESSAGE = 'N/A'.freeze
25
+ WIDTH = 80
26
+ IMPACT_SCORES = {
27
+ 'none' => 0.0,
28
+ 'low' => 0.1,
29
+ 'medium' => 0.4,
30
+ 'high' => 0.7,
31
+ 'critical' => 0.9
32
+ }.freeze
33
+
34
+ def self.parse_data_for_xccdf(json)
35
+ data = {}
36
+
37
+ controls = []
38
+ if json['profiles'].nil?
39
+ controls = json['controls']
40
+ elsif json['profiles'].length == 1
41
+ controls = json['profiles'].last['controls']
42
+ else
43
+ json['profiles'].each do |profile|
44
+ controls.concat(profile['controls'])
45
+ end
46
+ end
47
+ c_data = {}
48
+
49
+ controls.each do |control|
50
+ c_id = control['id'].to_sym
51
+ c_data[c_id] = {}
52
+ c_data[c_id]['id'] = control['id'] || DATA_NOT_FOUND_MESSAGE
53
+ c_data[c_id]['title'] = control['title'] || DATA_NOT_FOUND_MESSAGE
54
+ c_data[c_id]['desc'] = control['desc'] || DATA_NOT_FOUND_MESSAGE
55
+ c_data[c_id]['severity'] = control['tags']['severity'] || DATA_NOT_FOUND_MESSAGE
56
+ c_data[c_id]['gid'] = control['tags']['gid'] || DATA_NOT_FOUND_MESSAGE
57
+ c_data[c_id]['gtitle'] = control['tags']['gtitle'] || DATA_NOT_FOUND_MESSAGE
58
+ c_data[c_id]['gdescription'] = control['tags']['gdescription'] || DATA_NOT_FOUND_MESSAGE
59
+ c_data[c_id]['rid'] = control['tags']['rid'] || DATA_NOT_FOUND_MESSAGE
60
+ c_data[c_id]['rversion'] = control['tags']['rversion'] || DATA_NOT_FOUND_MESSAGE
61
+ c_data[c_id]['rweight'] = control['tags']['rweight'] || DATA_NOT_FOUND_MESSAGE
62
+ c_data[c_id]['stig_id'] = control['tags']['stig_id'] || DATA_NOT_FOUND_MESSAGE
63
+ c_data[c_id]['cci'] = control['tags']['cci'] || DATA_NOT_FOUND_MESSAGE
64
+ c_data[c_id]['nist'] = control['tags']['nist'] || ['unmapped']
65
+ c_data[c_id]['check'] = control['tags']['check'] || DATA_NOT_FOUND_MESSAGE
66
+ c_data[c_id]['checkref'] = control['tags']['checkref'] || DATA_NOT_FOUND_MESSAGE
67
+ c_data[c_id]['fix'] = control['tags']['fix'] || DATA_NOT_FOUND_MESSAGE
68
+ c_data[c_id]['fixref'] = control['tags']['fixref'] || DATA_NOT_FOUND_MESSAGE
69
+ c_data[c_id]['fix_id'] = control['tags']['fix_id'] || DATA_NOT_FOUND_MESSAGE
70
+ c_data[c_id]['rationale'] = control['tags']['rationale'] || DATA_NOT_FOUND_MESSAGE
71
+ c_data[c_id]['cis_family'] = control['tags']['cis_family'] || DATA_NOT_FOUND_MESSAGE
72
+ c_data[c_id]['cis_rid'] = control['tags']['cis_rid'] || DATA_NOT_FOUND_MESSAGE
73
+ c_data[c_id]['cis_level'] = control['tags']['cis_level'] || DATA_NOT_FOUND_MESSAGE
74
+ c_data[c_id]['impact'] = control['impact'].to_s || DATA_NOT_FOUND_MESSAGE
75
+ c_data[c_id]['code'] = control['code'].to_s || DATA_NOT_FOUND_MESSAGE
76
+ end
77
+
78
+ data['controls'] = c_data.values
79
+ data['status'] = 'success'
80
+ data
81
+ end
82
+
83
+ def self.parse_data_for_ckl(json)
84
+ data = {}
85
+
86
+ # Parse for inspec profile results json
87
+ json['profiles'].each do |profile|
88
+ profile['controls'].each do |control|
89
+ c_id = control['id'].to_sym
90
+ data[c_id] = {}
91
+
92
+ data[c_id][:vuln_num] = control['id'] unless control['id'].nil?
93
+ data[c_id][:rule_title] = control['title'] unless control['title'].nil?
94
+ data[c_id][:vuln_discuss] = control['desc'] unless control['desc'].nil?
95
+
96
+ unless control['tags'].nil?
97
+ data[c_id][:severity] = control['tags']['severity'] unless control['tags']['severity'].nil?
98
+ data[c_id][:gid] = control['tags']['gid'] unless control['tags']['gid'].nil?
99
+ data[c_id][:group_title] = control['tags']['gtitle'] unless control['tags']['gtitle'].nil?
100
+ data[c_id][:rule_id] = control['tags']['rid'] unless control['tags']['rid'].nil?
101
+ data[c_id][:rule_ver] = control['tags']['stig_id'] unless control['tags']['stig_id'].nil?
102
+ data[c_id][:cci_ref] = control['tags']['cci'] unless control['tags']['cci'].nil?
103
+ data[c_id][:nist] = control['tags']['nist'].join(' ') unless control['tags']['nist'].nil?
104
+ end
105
+
106
+ if control['descriptions'].respond_to?(:find)
107
+ data[c_id][:check_content] = control['descriptions'].find { |c| c['label'] == 'fix' }&.dig('data')
108
+ data[c_id][:fix_text] = control['descriptions'].find { |c| c['label'] == 'check' }&.dig('data')
109
+ end
110
+
111
+ data[c_id][:impact] = control['impact'].to_s unless control['impact'].nil?
112
+ data[c_id][:profile_name] = profile['name'].to_s unless profile['name'].nil?
113
+ data[c_id][:profile_shasum] = profile['sha256'].to_s unless profile['sha256'].nil?
114
+
115
+ data[c_id][:status] = []
116
+ data[c_id][:message] = []
117
+
118
+ if control.key?('results')
119
+ control['results'].each do |result|
120
+ if !result['backtrace'].nil?
121
+ result['status'] = 'error'
122
+ end
123
+ data[c_id][:status].push(result['status'])
124
+ data[c_id][:message].push("SKIPPED -- Test: #{result['code_desc']}\nMessage: #{result['skip_message']}\n") if result['status'] == 'skipped'
125
+ data[c_id][:message].push("FAILED -- Test: #{result['code_desc']}\nMessage: #{result['message']}\n") if result['status'] == 'failed'
126
+ data[c_id][:message].push("PASS -- #{result['code_desc']}\n") if result['status'] == 'passed'
127
+ data[c_id][:message].push("PROFILE_ERROR -- Test: #{result['code_desc']}\nMessage: #{result['backtrace']}\n") if result['status'] == 'error'
128
+ end
129
+ end
130
+
131
+ if data[c_id][:impact].to_f.zero?
132
+ data[c_id][:message].unshift("NOT_APPLICABLE -- Description: #{control['desc']}\n\n")
133
+ end
134
+ end
135
+ end
136
+ data
137
+ end
138
+
139
+ def self.get_platform(json)
140
+ json['profiles'].find { |profile| !profile[:platform].nil? }
141
+ end
142
+
143
+ def self.to_dotted_hash(hash, recursive_key = '')
144
+ hash.each_with_object({}) do |(k, v), ret|
145
+ key = recursive_key + k.to_s
146
+ if v.is_a? Hash
147
+ ret.merge! to_dotted_hash(v, key + '.')
148
+ else
149
+ ret[key] = v
150
+ end
151
+ end
152
+ end
153
+
154
+ def self.control_status(control, for_summary = false)
155
+ status_list = control[:status].uniq
156
+ if control[:impact].to_f.zero?
157
+ 'Not_Applicable'
158
+ elsif status_list.include?('failed')
159
+ 'Open'
160
+ elsif status_list.include?('passed')
161
+ 'NotAFinding'
162
+ elsif status_list.include?('error') && for_summary
163
+ 'Profile_Error'
164
+ else
165
+ # profile skipped or profile error
166
+ 'Not_Reviewed'
167
+ end
168
+ end
169
+
170
+ def self.control_finding_details(control, control_clk_status)
171
+ 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'
172
+ result = "All Automated tests passed for the control \n\n #{control[:message].join}" if control_clk_status == 'NotAFinding'
173
+ result = "Automated test skipped due to known accepted condition in the control : \n\n#{control[:message].join}" if control_clk_status == 'Not_Reviewed'
174
+ result = "Justification: \n #{control[:message].join}" if control_clk_status == 'Not_Applicable'
175
+ result = 'No test available or some test errors occurred for this control' if control_clk_status == 'Profile_Error'
176
+ result
177
+ end
178
+
179
+ # @!method get_impact(severity)
180
+ # Takes in the STIG severity tag and converts it to the InSpec #{impact}
181
+ # control tag.
182
+ # At the moment the mapping is static, so that:
183
+ # high => 0.7
184
+ # medium => 0.5
185
+ # low => 0.3
186
+ # @param severity [String] the string value you want to map to an InSpec
187
+ # 'impact' level.
188
+ #
189
+ # @return impact [Float] the impact level level mapped to the XCCDF severity
190
+ # mapped to a float between 0.0 - 1.0.
191
+ #
192
+ # @todo Allow for the user to pass in a hash for the desired mapping of text
193
+ # values to numbers or to override our hard coded values.
194
+ #
195
+ def self.get_impact(severity, use_cvss_terms: true)
196
+ return float_to_impact(severity, use_cvss_terms) if severity.is_a?(Float)
197
+
198
+ return string_to_impact(severity, use_cvss_terms) if severity.is_a?(String)
199
+
200
+ raise SeverityInputError, "'#{severity}' is not a valid severity value. It should be a Float between 0.0 and " \
201
+ '1.0 or one of the approved keywords.'
202
+ end
203
+
204
+ private_class_method def self.float_to_impact(severity, use_cvss_terms)
205
+ unless severity.between?(0, 1)
206
+ raise SeverityInputError, "'#{severity}' is not a valid severity value. It should be a Float between 0.0 and " \
207
+ '1.0 or one of the approved keywords.'
208
+ end
209
+
210
+ if severity <= 0.01
211
+ 0.0 # Informative
212
+ elsif severity < 0.4
213
+ 0.3 # Low Impact
214
+ elsif severity < 0.7
215
+ 0.5 # Medium Impact
216
+ elsif severity < 0.9 || use_cvss_terms
217
+ 0.7 # High Impact
218
+ else
219
+ 1.0 # Critical Controls
220
+ end
221
+ end
222
+
223
+ private_class_method def self.string_to_impact(severity, use_cvss_terms)
224
+ if /none|na|n\/a|not[_|(\s*)]?applicable/i.match?(severity)
225
+ impact = 0.0 # Informative
226
+ elsif /low|cat(egory)?\s*(iii|3)/i.match?(severity)
227
+ impact = 0.3 # Low Impact
228
+ elsif /med(ium)?|cat(egory)?\s*(ii|2)/i.match?(severity)
229
+ impact = 0.5 # Medium Impact
230
+ elsif /high|cat(egory)?\s*(i|1)/i.match?(severity)
231
+ impact = 0.7 # High Impact
232
+ elsif /crit(ical)?|severe/i.match?(severity)
233
+ impact = 1.0 # Critical Controls
234
+ else
235
+ raise SeverityInputError, "'#{severity}' is not a valid severity value. It should be a Float between 0.0 and " \
236
+ '1.0 or one of the approved keywords.'
237
+ end
238
+
239
+ impact == 1.0 && use_cvss_terms ? 0.7 : impact
240
+ end
241
+
242
+ def self.get_impact_string(impact, use_cvss_terms: true)
243
+ return if impact.nil?
244
+
245
+ value = impact.to_f
246
+ unless value.between?(0, 1)
247
+ raise ImpactInputError, "'#{value}' is not a valid impact score. Valid impact scores: [0.0 - 1.0]."
248
+ end
249
+
250
+ IMPACT_SCORES.reverse_each do |name, impact_score|
251
+ if name == 'critical' && value >= impact_score && use_cvss_terms
252
+ return 'high'
253
+ elsif value >= impact_score
254
+ return name
255
+ else
256
+ next
257
+ end
258
+ end
259
+ end
260
+
261
+ def self.unpack_inspec_json(directory, inspec_json, separated, output_format)
262
+ if directory == 'id'
263
+ directory = inspec_json['name']
264
+ end
265
+ controls = generate_controls(inspec_json)
266
+ unpack_profile(directory || 'profile', controls, separated, output_format || 'json')
267
+ create_inspec_yml(directory || 'profile', inspec_json)
268
+ create_license(directory || 'profile', inspec_json)
269
+ create_readme_md(directory || 'profile', inspec_json)
270
+ end
271
+
272
+ private_class_method def self.wrap(str, width = WIDTH)
273
+ str.gsub!("desc \"\n ", 'desc "')
274
+ str.gsub!(/\\r/, "\n")
275
+ str.gsub!(/\\n/, "\n")
276
+
277
+ WordWrap.ww(str.to_s, width)
278
+ end
279
+
280
+ private_class_method def self.generate_controls(inspec_json)
281
+ controls = []
282
+ inspec_json['controls'].each do |json_control|
283
+ control = ::Inspec::Object::Control.new
284
+ if (defined? control.desc).nil?
285
+ control.descriptions[:default] = json_control['desc']
286
+ control.descriptions[:rationale] = json_control['tags']['rationale']
287
+ control.descriptions[:check] = json_control['tags']['check']
288
+ control.descriptions[:fix] = json_control['tags']['fix']
289
+ else
290
+ control.desc = json_control['desc']
291
+ end
292
+ control.id = json_control['id']
293
+ control.title = json_control['title']
294
+ control.impact = get_impact(json_control['impact'])
295
+
296
+ # json_control['tags'].each do |tag|
297
+ # control.add_tag(Inspec::Object::Tag.new(tag.key, tag.value)
298
+ # end
299
+
300
+ control.add_tag(::Inspec::Object::Tag.new('severity', json_control['tags']['severity']))
301
+ control.add_tag(::Inspec::Object::Tag.new('gtitle', json_control['tags']['gtitle']))
302
+ control.add_tag(::Inspec::Object::Tag.new('satisfies', json_control['tags']['satisfies'])) if json_control['tags']['satisfies']
303
+ control.add_tag(::Inspec::Object::Tag.new('gid', json_control['tags']['gid']))
304
+ control.add_tag(::Inspec::Object::Tag.new('rid', json_control['tags']['rid']))
305
+ control.add_tag(::Inspec::Object::Tag.new('stig_id', json_control['tags']['stig_id']))
306
+ control.add_tag(::Inspec::Object::Tag.new('fix_id', json_control['tags']['fix_id']))
307
+ control.add_tag(::Inspec::Object::Tag.new('cci', json_control['tags']['cci']))
308
+ control.add_tag(::Inspec::Object::Tag.new('nist', json_control['tags']['nist']))
309
+ control.add_tag(::Inspec::Object::Tag.new('cis_level', json_control['tags']['cis_level'])) unless json_control['tags']['cis_level'].blank?
310
+ control.add_tag(::Inspec::Object::Tag.new('cis_controls', json_control['tags']['cis_controls'])) unless json_control['tags']['cis_controls'].blank?
311
+ control.add_tag(::Inspec::Object::Tag.new('cis_rid', json_control['tags']['cis_rid'])) unless json_control['tags']['cis_rid'].blank?
312
+ control.add_tag(::Inspec::Object::Tag.new('ref', json_control['tags']['ref'])) unless json_control['tags']['ref'].blank?
313
+ control.add_tag(::Inspec::Object::Tag.new('false_negatives', json_control['tags']['false_negatives'])) unless json_control['tags']['false_positives'].blank?
314
+ control.add_tag(::Inspec::Object::Tag.new('false_positives', json_control['tags']['false_positives'])) unless json_control['tags']['false_positives'].blank?
315
+ control.add_tag(::Inspec::Object::Tag.new('documentable', json_control['tags']['documentable'])) unless json_control['tags']['documentable'].blank?
316
+ control.add_tag(::Inspec::Object::Tag.new('mitigations', json_control['tags']['mitigations'])) unless json_control['tags']['mitigations'].blank?
317
+ control.add_tag(::Inspec::Object::Tag.new('severity_override_guidance', json_control['tags']['severity_override_guidance'])) unless json_control['tags']['severity_override_guidance'].blank?
318
+ control.add_tag(::Inspec::Object::Tag.new('security_override_guidance', json_control['tags']['security_override_guidance'])) unless json_control['tags']['security_override_guidance'].blank?
319
+ control.add_tag(::Inspec::Object::Tag.new('potential_impacts', json_control['tags']['potential_impacts'])) unless json_control['tags']['potential_impacts'].blank?
320
+ control.add_tag(::Inspec::Object::Tag.new('third_party_tools', json_control['tags']['third_party_tools'])) unless json_control['tags']['third_party_tools'].blank?
321
+ control.add_tag(::Inspec::Object::Tag.new('mitigation_controls', json_control['tags']['mitigation_controls'])) unless json_control['tags']['mitigation_controls'].blank?
322
+ control.add_tag(::Inspec::Object::Tag.new('responsibility', json_control['tags']['responsibility'])) unless json_control['tags']['responsibility'].blank?
323
+ control.add_tag(::Inspec::Object::Tag.new('ia_controls', json_control['tags']['ia_controls'])) unless json_control['tags']['ia_controls'].blank?
324
+
325
+ controls << control
326
+ end
327
+ controls
328
+ end
329
+
330
+ # @!method print_benchmark_info(info)
331
+ # writes benchmark info to profile inspec.yml file
332
+ #
333
+ private_class_method def self.create_inspec_yml(directory, inspec_json)
334
+ benchmark_info =
335
+ "name: #{inspec_json['name']}\n" \
336
+ "title: #{inspec_json['title']}\n" \
337
+ "maintainer: #{inspec_json['maintainer']}\n" \
338
+ "copyright: #{inspec_json['copyright']}\n" \
339
+ "copyright_email: #{inspec_json['copyright_email']}\n" \
340
+ "license: #{inspec_json['license']}\n" \
341
+ "summary: #{inspec_json['summary']}\n" \
342
+ "version: #{inspec_json['version']}\n"
343
+
344
+ myfile = File.new("#{directory}/inspec.yml", 'w')
345
+ myfile.puts benchmark_info
346
+ end
347
+
348
+ private_class_method def self.create_license(directory, inspec_json)
349
+ license_content = ''
350
+ if !inspec_json['license'].nil?
351
+ begin
352
+ response = Net::HTTP.get_response(URI(inspec_json['license']))
353
+ if response.code == '200'
354
+ license_content = response.body
355
+ else
356
+ license_content = inspec_json['license']
357
+ end
358
+ rescue StandardError => _e
359
+ license_content = inspec_json['license']
360
+ end
361
+ end
362
+
363
+ myfile = File.new("#{directory}/LICENSE", 'w')
364
+ myfile.puts license_content
365
+ end
366
+
367
+ private_class_method def self.create_readme_md(directory, inspec_json)
368
+ readme_contents =
369
+ "\# #{inspec_json['title']}\n" \
370
+ "#{inspec_json['summary']}\n" \
371
+ "---\n" \
372
+ "Name: #{inspec_json['name']}\n" \
373
+ "Author: #{inspec_json['maintainer']}\n" \
374
+ "Status: #{inspec_json['status']}\n" \
375
+ "Copyright: #{inspec_json['copyright']}\n" \
376
+ "Copyright Email: #{inspec_json['copyright_email']}\n" \
377
+ "Version: #{inspec_json['version']}\n" \
378
+ "#{inspec_json['plaintext']}\n" \
379
+ "Reference: #{inspec_json['reference_href']}\n" \
380
+ "Reference by: #{inspec_json['reference_publisher']}\n" \
381
+ "Reference source: #{inspec_json['reference_source']}\n"
382
+
383
+ myfile = File.new("#{directory}/README.md", 'w')
384
+ myfile.puts readme_contents
385
+ end
386
+
387
+ private_class_method def self.unpack_profile(directory, controls, separated, output_format)
388
+ FileUtils.rm_rf(directory) if Dir.exist?(directory)
389
+ Dir.mkdir directory unless Dir.exist?(directory)
390
+ Dir.mkdir "#{directory}/controls" unless Dir.exist?("#{directory}/controls")
391
+ Dir.mkdir "#{directory}/libraries" unless Dir.exist?("#{directory}/libraries")
392
+ if separated
393
+ if output_format == 'ruby'
394
+ controls.each do |control|
395
+ file_name = control.id.to_s
396
+ myfile = File.new("#{directory}/controls/#{file_name}.rb", 'w')
397
+ myfile.puts "# encoding: UTF-8\n\n"
398
+ myfile.puts wrap(control.to_ruby, WIDTH) + "\n"
399
+ myfile.close
400
+ end
401
+ else
402
+ controls.each do |control|
403
+ file_name = control.id.to_s
404
+ myfile = File.new("#{directory}/controls/#{file_name}.rb", 'w')
405
+ PP.pp(control.to_hash, myfile)
406
+ myfile.close
407
+ end
408
+ end
409
+ else
410
+ myfile = File.new("#{directory}/controls/controls.rb", 'w')
411
+ if output_format == 'ruby'
412
+ controls.each do |control|
413
+ myfile.puts "# encoding: UTF-8\n\n"
414
+ myfile.puts wrap(control.to_ruby.gsub('"', "\'"), WIDTH) + "\n"
415
+ end
416
+ else
417
+ controls.each do |control|
418
+ if (defined? control.desc).nil?
419
+ control.descriptions[:default].strip!
420
+ else
421
+ control.desc.strip!
422
+ end
423
+
424
+ PP.pp(control.to_hash, myfile)
425
+ end
426
+ end
427
+ myfile.close
428
+ end
429
+ config_store = ::RuboCop::ConfigStore.new
430
+ config_store.options_config = File.join(File.dirname(__FILE__), '../data/rubocop.yml')
431
+ rubocop = ::RuboCop::Runner.new({ auto_correct: true }, config_store)
432
+ rubocop.run([directory])
433
+ end
434
+ end
435
+ end
436
+
437
+ # rubocop:enable Metrics/ClassLength
438
+ # rubocop:enable Metrics/AbcSize
439
+ # rubocop:enable Metrics/PerceivedComplexity
440
+ # rubocop:enable Metrics/CyclomaticComplexity
441
+ # rubocop:enable Metrics/MethodLength