inspec_tools 2.0.7

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 (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