heimdall_tools 1.3.32 → 1.3.37

Sign up to get free protection for your applications and to get access to all the features.
@@ -11,4 +11,8 @@ module HeimdallTools
11
11
  autoload :BurpSuiteMapper, 'heimdall_tools/burpsuite_mapper'
12
12
  autoload :NessusMapper, 'heimdall_tools/nessus_mapper'
13
13
  autoload :SnykMapper, 'heimdall_tools/snyk_mapper'
14
+ autoload :NiktoMapper, 'heimdall_tools/nikto_mapper'
15
+ autoload :JfrogXrayMapper, 'heimdall_tools/jfrog_xray_mapper'
16
+ autoload :DBProtectMapper, 'heimdall_tools/dbprotect_mapper'
17
+ autoload :AwsConfigMapper, 'heimdall_tools/aws_config_mapper'
14
18
  end
@@ -0,0 +1,284 @@
1
+ require 'aws-sdk-configservice'
2
+ require 'heimdall_tools/hdf'
3
+ require 'csv'
4
+ require 'json'
5
+
6
+ RESOURCE_DIR = Pathname.new(__FILE__).join('../../data')
7
+
8
+ AWS_CONFIG_MAPPING_FILE = File.join(RESOURCE_DIR, 'aws-config-mapping.csv')
9
+
10
+ NOT_APPLICABLE_MSG = 'No AWS resources found to evaluate complaince for this rule'.freeze
11
+ INSUFFICIENT_DATA_MSG = 'Not enough data has been collectd to determine compliance yet.'.freeze
12
+
13
+ ##
14
+ # HDF mapper for use with AWS Config rules.
15
+ #
16
+ # Ruby AWS Ruby SDK for ConfigService:
17
+ # - https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/ConfigService/Client.html
18
+ #
19
+ # rubocop:disable Metrics/AbcSize, Metrics/ClassLength
20
+ module HeimdallTools
21
+ class AwsConfigMapper
22
+ def initialize(custom_mapping, verbose = false)
23
+ @verbose = verbose
24
+ @default_mapping = get_rule_mapping(AWS_CONFIG_MAPPING_FILE)
25
+ @custom_mapping = custom_mapping.nil? ? {} : get_rule_mapping(custom_mapping)
26
+ @client = Aws::ConfigService::Client.new
27
+ @issues = get_all_config_rules
28
+ end
29
+
30
+ ##
31
+ # Convert to HDF
32
+ #
33
+ # If there is overlap in rule names from @default_mapping and @custom_mapping,
34
+ # then the tags from both will be added to the rule.
35
+ def to_hdf
36
+ controls = @issues.map do |issue|
37
+ @item = {}
38
+ @item['id'] = issue[:config_rule_name]
39
+ @item['title'] = issue[:config_rule_name]
40
+ @item['desc'] = issue[:description]
41
+ @item['impact'] = 0.5
42
+ @item['tags'] = hdf_tags(issue)
43
+ @item['descriptions'] = hdf_descriptions(issue)
44
+ @item['refs'] = NA_ARRAY
45
+ @item['source_location'] = { ref: issue[:config_rule_arn], line: 1 }
46
+ @item['code'] = ''
47
+ @item['results'] = issue[:results]
48
+ # Avoid duplicating rules that exist in the custom mapping as 'unmapped' in this loop
49
+ if @custom_mapping.include?(issue[:config_rule_name]) && !@default_mapping.include?(issue[:config_rule_name])
50
+ nil
51
+ else
52
+ @item
53
+ end
54
+ end
55
+ results = HeimdallDataFormat.new(
56
+ profile_name: 'AWS Config',
57
+ title: 'AWS Config',
58
+ summary: 'AWS Config',
59
+ controls: controls,
60
+ statistics: { aws_config_sdk_version: Aws::ConfigService::GEM_VERSION }
61
+ )
62
+ results.to_hdf
63
+ end
64
+
65
+ private
66
+
67
+ ##
68
+ # Read in a config rule -> 800-53 control mapping CSV.
69
+ #
70
+ # Params:
71
+ # - path: The file path to the CSV file
72
+ #
73
+ # Returns: A mapped version of the csv in the format { rule_name: row, ... }
74
+ def get_rule_mapping(path)
75
+ Hash[CSV.read(path, headers: true).map { |row| [row[0], row] }]
76
+ end
77
+
78
+ ##
79
+ # Fetches information on all of the config rules available to the
80
+ # AWS account.
81
+ #
82
+ # https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/ConfigService/Client.html#describe_config_rules-instance_method
83
+ #
84
+ # Returns: list of hash for all config rules available
85
+ def get_all_config_rules
86
+ config_rules = []
87
+
88
+ # Fetch all rules with pagination
89
+ response = @client.describe_config_rules
90
+ config_rules += response.config_rules
91
+ while response.next_token
92
+ response = @client.describe_config_rules(next_token: response.next_token)
93
+ config_rules += response.config_rules
94
+ end
95
+ config_rules = config_rules.map(&:to_h)
96
+
97
+ # Add necessary data to rules using helpers
98
+ add_compliance_to_config_rules(config_rules)
99
+ add_results_to_config_rules(config_rules)
100
+ end
101
+
102
+ ##
103
+ # Adds compliance information for config rules to the config rule hash
104
+ # from AwsConfigMapper::get_all_config_rules.
105
+ #
106
+ # `complaince_type` may be any of the following:
107
+ # ["COMPLIANT", "NON_COMPLIANT", "NOT_APPLICABLE", "INSUFFICIENT_DATA"]
108
+ #
109
+ # Params:
110
+ # - config_rules: The list of hash from AwsConfigMapper::get_all_config_rules
111
+ #
112
+ # Returns: The same config_rules array with `compliance` key added to each rule
113
+ def add_compliance_to_config_rules(config_rules)
114
+ mapped_compliance_results = fetch_all_compliance_info(config_rules)
115
+
116
+ # Add compliance to config_rules
117
+ config_rules.each do |rule|
118
+ rule[:compliance] = mapped_compliance_results[rule[:config_rule_name]]&.dig(:compliance, :compliance_type)
119
+ end
120
+
121
+ config_rules
122
+ end
123
+
124
+ ##
125
+ # Fetch and combine all compliance information for the config rules.
126
+ #
127
+ # AWS allows passing up to 25 rules at a time to this endpoint.
128
+ #
129
+ # https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/ConfigService/Client.html#describe_compliance_by_config_rule-instance_method
130
+ #
131
+ # Params:
132
+ # - config_rules: The list of hash from AwsConfigMapper::get_all_config_rules
133
+ #
134
+ # Returns: Results mapped by config rule in the format { name: {<response>}, ... }
135
+ def fetch_all_compliance_info(config_rules)
136
+ compliance_results = []
137
+
138
+ config_rules.each_slice(25).each do |slice|
139
+ config_rule_names = slice.map { |r| r[:config_rule_name] }
140
+ response = @client.describe_compliance_by_config_rule(config_rule_names: config_rule_names)
141
+ compliance_results += response.compliance_by_config_rules
142
+ end
143
+
144
+ # Map based on name for easy lookup
145
+ Hash[compliance_results.collect { |r| [r.config_rule_name, r.to_h] }]
146
+ end
147
+
148
+ ##
149
+ # Takes in config rules and formats the results for hdf format.
150
+ #
151
+ # https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/ConfigService/Client.html#get_compliance_details_by_config_rule-instance_method
152
+ #
153
+ # Example hdf results:
154
+ # [
155
+ # {
156
+ # "code_desc": "This rule...",
157
+ # "run_time": 0.314016,
158
+ # "start_time": "2018-11-18T20:21:40-05:00",
159
+ # "status": "passed"
160
+ # },
161
+ # ...
162
+ # ]
163
+ #
164
+ # Status may be any of the following: ['passed', 'failed', 'skipped', 'loaded']
165
+ #
166
+ # Params:
167
+ # - rule: Rules from AwsConfigMapper::get_all_config_rules
168
+ #
169
+ # Returns: The same config_rules array with `results` key added to each rule.
170
+ def add_results_to_config_rules(config_rules)
171
+ config_rules.each do |rule|
172
+ response = @client.get_compliance_details_by_config_rule(config_rule_name: rule[:config_rule_name], limit: 100)
173
+ rule_results = response.to_h[:evaluation_results]
174
+ while response.next_token
175
+ response = @client.get_compliance_details_by_config_rule(next_token: response.next_token, limit: 100)
176
+ rule_results += response.to_h[:evaluation_results]
177
+ end
178
+
179
+ rule[:results] = []
180
+ rule_results.each do |result|
181
+ hdf_result = {}
182
+ # code_desc
183
+ hdf_result['code_desc'] = result.dig(:evaluation_result_identifier, :evaluation_result_qualifier)&.map do |k, v|
184
+ "#{k}: #{v}"
185
+ end&.join(', ')
186
+ # start_time
187
+ hdf_result['start_time'] = if result.key?(:config_rule_invoked_time)
188
+ DateTime.parse(result[:config_rule_invoked_time].to_s).strftime('%Y-%m-%dT%H:%M:%S%:z')
189
+ end
190
+ # run_time
191
+ hdf_result['run_time'] = if result.key?(:result_recorded_time) && result.key?(:config_rule_invoked_time)
192
+ (result[:result_recorded_time] - result[:config_rule_invoked_time]).round(6)
193
+ end
194
+ # status
195
+ hdf_result['status'] = case result.dig(:compliance_type)
196
+ when 'COMPLIANT'
197
+ 'passed'
198
+ when 'NON_COMPLIANT'
199
+ 'failed'
200
+ else
201
+ 'skipped'
202
+ end
203
+ hdf_result['message'] = "(#{hdf_result['code_desc']}): #{result[:annotation] || 'Rule does not pass rule compliance'}" if hdf_result['status'] == 'failed'
204
+ rule[:results] << hdf_result
205
+ end
206
+ next unless rule[:results].empty?
207
+
208
+ case rule[:compliance]
209
+ when 'NOT_APPLICABLE'
210
+ rule[:impact] = 0
211
+ rule[:results] << {
212
+ 'run_time': 0,
213
+ 'code_desc': NOT_APPLICABLE_MSG,
214
+ 'skip_message': NOT_APPLICABLE_MSG,
215
+ 'start_time': DateTime.now.strftime('%Y-%m-%dT%H:%M:%S%:z'),
216
+ 'status': 'skipped'
217
+ }
218
+ when 'INSUFFICIENT_DATA'
219
+ rule[:results] << {
220
+ 'run_time': 0,
221
+ 'code_desc': INSUFFICIENT_DATA_MSG,
222
+ 'skip_message': INSUFFICIENT_DATA_MSG,
223
+ 'start_time': DateTime.now.strftime('%Y-%m-%dT%H:%M:%S%:z'),
224
+ 'status': 'skipped'
225
+ }
226
+ end
227
+ end
228
+
229
+ config_rules
230
+ end
231
+
232
+ ##
233
+ # Takes in a config rule and pulls out tags that are useful for HDF.
234
+ #
235
+ # Params:
236
+ # - config_rule: A single config rule from AwsConfigMapper::get_all_config_rules
237
+ #
238
+ # Returns: Hash containing all relevant HDF tags
239
+ def hdf_tags(config_rule)
240
+ result = {}
241
+
242
+ @default_mapping
243
+ @custom_mapping
244
+
245
+ # NIST tag
246
+ result['nist'] = []
247
+ default_mapping_match = @default_mapping[config_rule[:config_rule_name]]
248
+
249
+ result['nist'] += default_mapping_match[1].split('|') unless default_mapping_match.nil?
250
+
251
+ custom_mapping_match = @custom_mapping[config_rule[:config_rule_name]]
252
+
253
+ result['nist'] += custom_mapping_match[1].split('|').map { |name| "#{name} (user provided)" } unless custom_mapping_match.nil?
254
+
255
+ result['nist'] = ['unmapped'] if result['nist'].empty?
256
+
257
+ result
258
+ end
259
+
260
+ def check_text(config_rule)
261
+ params = (JSON.parse(config_rule[:input_parameters]).map { |key, value| "#{key}: #{value}" }).join('<br/>')
262
+ check_text = config_rule[:config_rule_arn]
263
+ check_text += "<br/>#{params}" unless params.empty?
264
+ check_text
265
+ end
266
+
267
+ ##
268
+ # Takes in a config rule and pulls out information for the descriptions array
269
+ #
270
+ # Params:
271
+ # - config_rule: A single config rule from AwsConfigMapper::get_all_config_rules
272
+ #
273
+ # Returns: Array containing all relevant descriptions information
274
+ def hdf_descriptions(config_rule)
275
+ [
276
+ {
277
+ 'label': 'check',
278
+ 'data': check_text(config_rule)
279
+ }
280
+ ]
281
+ end
282
+ end
283
+ end
284
+ # rubocop:enable Metrics/AbcSize, Metrics/ClassLength
@@ -61,8 +61,8 @@ module HeimdallTools
61
61
 
62
62
  end
63
63
 
64
- desc 'snyk_mapper', 'snyk_mapper translates Synk results Json to HDF format Json be viewed on Heimdall'
65
- long_desc Help.text(:fortify_mapper)
64
+ desc 'snyk_mapper', 'snyk_mapper translates Snyk results Json to HDF format Json be viewed on Heimdall'
65
+ long_desc Help.text(:snyk_mapper)
66
66
  option :json, required: true, aliases: '-j'
67
67
  option :output_prefix, required: true, aliases: '-o'
68
68
  option :verbose, type: :boolean, aliases: '-V'
@@ -75,6 +75,54 @@ module HeimdallTools
75
75
  end
76
76
  end
77
77
 
78
+ desc 'nikto_mapper', 'nikto_mapper translates Nikto results Json to HDF format Json be viewed on Heimdall'
79
+ long_desc Help.text(:nikto_mapper)
80
+ option :json, required: true, aliases: '-j'
81
+ option :output, required: true, aliases: '-o'
82
+ option :verbose, type: :boolean, aliases: '-V'
83
+ def nikto_mapper
84
+ hdf = HeimdallTools::NiktoMapper.new(File.read(options[:json])).to_hdf
85
+ File.write(options[:output], hdf)
86
+ puts "\r\HDF Generated:\n"
87
+ puts "#{options[:output]}"
88
+ end
89
+
90
+ desc 'jfrog_xray_mapper', 'jfrog_xray_mapper translates Jfrog Xray results Json to HDF format Json be viewed on Heimdall'
91
+ long_desc Help.text(:jfrog_xray_mapper)
92
+ option :json, required: true, aliases: '-j'
93
+ option :output, required: true, aliases: '-o'
94
+ option :verbose, type: :boolean, aliases: '-V'
95
+ def jfrog_xray_mapper
96
+ hdf = HeimdallTools::JfrogXrayMapper.new(File.read(options[:json])).to_hdf
97
+ File.write(options[:output], hdf)
98
+ puts "\r\HDF Generated:\n"
99
+ puts "#{options[:output]}"
100
+ end
101
+
102
+ desc 'dbprotect_mapper', 'dbprotect_mapper translates dbprotect results xml to HDF format Json be viewed on Heimdall'
103
+ long_desc Help.text(:dbprotect_mapper)
104
+ option :xml, required: true, aliases: '-x'
105
+ option :output, required: true, aliases: '-o'
106
+ option :verbose, type: :boolean, aliases: '-V'
107
+ def dbprotect_mapper
108
+ hdf = HeimdallTools::DBProtectMapper.new(File.read(options[:xml])).to_hdf
109
+ File.write(options[:output], hdf)
110
+ puts "\r\HDF Generated:\n"
111
+ puts "#{options[:output]}"
112
+ end
113
+
114
+ desc 'aws_config_mapper', 'aws_config_mapper pulls Ruby AWS SDK data to translate AWS Config Rule results into HDF format Json to be viewable in Heimdall'
115
+ long_desc Help.text(:aws_config_mapper)
116
+ # option :custom_mapping, required: false, aliases: '-m'
117
+ option :output, required: true, aliases: '-o'
118
+ option :verbose, type: :boolean, aliases: '-V'
119
+ def aws_config_mapper
120
+ hdf = HeimdallTools::AwsConfigMapper.new(options[:custom_mapping]).to_hdf
121
+ File.write(options[:output], hdf)
122
+ puts "\r\HDF Generated:\n"
123
+ puts "#{options[:output]}"
124
+ end
125
+
78
126
  desc 'version', 'prints version'
79
127
  def version
80
128
  puts VERSION
@@ -0,0 +1,127 @@
1
+ require 'json'
2
+ require 'csv'
3
+ require 'heimdall_tools/hdf'
4
+ require 'utilities/xml_to_hash'
5
+
6
+ IMPACT_MAPPING = {
7
+ High: 0.7,
8
+ Medium: 0.5,
9
+ Low: 0.3,
10
+ Informational: 0.0
11
+ }.freeze
12
+
13
+ # rubocop:disable Metrics/AbcSize
14
+
15
+ module HeimdallTools
16
+ class DBProtectMapper
17
+ def initialize(xml, name=nil, verbose = false)
18
+ @verbose = verbose
19
+
20
+ begin
21
+ dataset = xml_to_hash(xml)
22
+ @entries = compile_findings(dataset['dataset'])
23
+
24
+ rescue StandardError => e
25
+ raise "Invalid DBProtect XML file provided Exception: #{e};\nNote that XML must be of kind `Check Results Details`."
26
+ end
27
+
28
+ end
29
+
30
+ def to_hdf
31
+ controls = []
32
+ @entries.each do |entry|
33
+ @item = {}
34
+ @item['id'] = entry['Check ID']
35
+ @item['title'] = entry['Check']
36
+ @item['desc'] = format_desc(entry)
37
+ @item['impact'] = impact(entry['Risk DV'])
38
+ @item['tags'] = {}
39
+ @item['descriptions'] = []
40
+ @item['refs'] = NA_ARRAY
41
+ @item['source_location'] = NA_HASH
42
+ @item['code'] = ''
43
+ @item['results'] = finding(entry)
44
+
45
+ controls << @item
46
+ end
47
+ controls = collapse_duplicates(controls)
48
+ results = HeimdallDataFormat.new(profile_name: @entries.first['Policy'],
49
+ version: "",
50
+ title: @entries.first['Job Name'],
51
+ summary: format_summary(@entries.first),
52
+ controls: controls)
53
+ results.to_hdf
54
+ end
55
+
56
+ private
57
+
58
+ def compile_findings(dataset)
59
+ keys = dataset['metadata']['item'].map{ |e| e['name']}
60
+ findings = dataset['data']['row'].map { |e| Hash[keys.zip(e['value'])] }
61
+ findings
62
+ end
63
+
64
+ def format_desc(entry)
65
+ text = []
66
+ text << "Task : #{entry['Task']}"
67
+ text << "Check Category : #{entry['Check Category']}"
68
+ text.join("; ")
69
+ end
70
+
71
+ def format_summary(entry)
72
+ text = []
73
+ text << "Organization : #{entry['Organization']}"
74
+ text << "Asset : #{entry['Check Asset']}"
75
+ text << "Asset Type : #{entry['Asset Type']}"
76
+ text << "IP Address, Port, Instance : #{entry['Asset Type']}"
77
+ text << "IP Address, Port, Instance : #{entry['IP Address, Port, Instance']}"
78
+ text.join("\n")
79
+ end
80
+
81
+ def finding(entry)
82
+ finding = {}
83
+
84
+ finding['code_desc'] = entry['Details']
85
+ finding['run_time'] = 0.0
86
+ finding['start_time'] = entry['Date']
87
+
88
+ case entry['Result Status']
89
+ when 'Fact'
90
+ finding['status'] = 'skipped'
91
+ when 'Failed'
92
+ finding['status'] = 'failed'
93
+ finding['backtrace'] = ["DB Protect Failed Check"]
94
+ when 'Finding'
95
+ finding['status'] = 'failed'
96
+ when 'Not A Finding'
97
+ finding['status'] = 'passed'
98
+ when 'Skipped'
99
+ finding['status'] = 'skipped'
100
+ else
101
+ finding['status'] = 'skipped'
102
+ end
103
+ [finding]
104
+ end
105
+
106
+ def impact(severity)
107
+ IMPACT_MAPPING[severity.to_sym]
108
+ end
109
+
110
+ # DBProtect report could have multiple issue entries for multiple findings of same issue type.
111
+ # The meta data is identical across entries
112
+ # method collapse_duplicates return unique controls with applicable findings collapsed into it.
113
+ def collapse_duplicates(controls)
114
+ unique_controls = []
115
+
116
+ controls.map { |x| x['id'] }.uniq.each do |id|
117
+ collapsed_results = controls.select { |x| x['id'].eql?(id) }.map {|x| x['results']}
118
+ unique_control = controls.find { |x| x['id'].eql?(id) }
119
+ unique_control['results'] = collapsed_results.flatten
120
+ unique_controls << unique_control
121
+ end
122
+ unique_controls
123
+ end
124
+
125
+
126
+ end
127
+ end