nexpose_servicenow 0.5.1 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7f3da5fa2194d025ab5192815f682a09a94583be
4
- data.tar.gz: 94e1f6a2c0976fba3215185d62a2f0a03edcb5c5
3
+ metadata.gz: 293f877592c7698b0730bed8471ac8d9557009b3
4
+ data.tar.gz: 56922788a7b2b7a399b9146c8075f2cea9d013ad
5
5
  SHA512:
6
- metadata.gz: 6c6223fa14d6e6b0221e6176f5a4b79c4e9816dc2350592b68262cc94a98cf2cdf26d3bc647f699102c20954f9120fa12fdb1d0d4aa041a9c8cda75748036322
7
- data.tar.gz: b5f07c1718b8cf959b932d6d76192c8ccd2a71b30ec70805334ab117f2e4f3b0e373ac2c3e876dced59f2e2d7f1dabc427568dd547c854cb636f6700c2b1fb2b
6
+ metadata.gz: 2d0cf6ebf473a19eb15497b70344c0f332cfcffda10c09cbb0d07c119e057cf4633aac4dc6ba77849c400d31129e3ca325157e66bf207a427309c2a8fd28b4f3
7
+ data.tar.gz: 55a7cd2740faf850e6534e0aa4f240ef2dff326ec968cafd458b0920d1b336701e5d4fdf150ed0d89cfe7467a3bb23aa7f5f63c254cd5c98501b6c2db3a56858
@@ -84,8 +84,8 @@ module NexposeServiceNow
84
84
  # Create a report if explicitly required or else an existing
85
85
  # report file isn't found
86
86
  def self.create_report(report_details, options)
87
- if options[:mode].start_with? 'update_' or
88
- (options[:mode] == 'latest_scans' && options[:id_type] != :site)
87
+ if %w(update_ remove_).any? { |m| options[:mode].start_with? m} or
88
+ (options[:mode] == 'latest_scans' && options[:id_type] != :site)
89
89
  return
90
90
  end
91
91
 
@@ -199,5 +199,10 @@ module NexposeServiceNow
199
199
  historical_data.set_last_vuln(options[:last_scan_data],
200
200
  options[:nexpose_ids])
201
201
  end
202
+
203
+ def self.remove_diff_comparison_mode(report_details, options)
204
+ historical_data = get_historical_data(options)
205
+ historical_data.remove_last_diff_comparison_data options[:output_dir]
206
+ end
202
207
  end
203
208
  end
@@ -0,0 +1,140 @@
1
+ require 'fileutils'
2
+ require 'tempfile'
3
+ require 'csv-diff'
4
+ require 'csv'
5
+
6
+ module NexposeServiceNow
7
+ class CsvCompare
8
+ def self.get_columns(csv_file)
9
+ columns = ''
10
+ File::open(csv_file,'r') do |f|
11
+ columns = f.readline.rstrip
12
+ end
13
+
14
+ columns.split(',')
15
+ end
16
+
17
+ def self.get_row(value, columns, status)
18
+ columns.map { |c| value.fields[c] }.push(status)
19
+ end
20
+
21
+ def self.get_delete(value, columns)
22
+ self.get_row(value, columns, 'old')
23
+ end
24
+
25
+ def self.get_add(value, columns)
26
+ self.get_row(value, columns, 'new')
27
+ end
28
+
29
+ def self.update_to_old(value, columns)
30
+ self.update_to_row(value, columns, 0, 'old')
31
+ end
32
+
33
+ def self.update_to_new(value, columns)
34
+ self.update_to_row(value, columns, 1, 'new')
35
+ end
36
+
37
+ def self.update_to_row(value, columns, index, status)
38
+ row = []
39
+ columns.each do |c|
40
+ val = value.fields[c]
41
+ row << (val.kind_of?(Array) ? val[index] : val)
42
+ end
43
+ row.push(status)
44
+ end
45
+
46
+ def self.append_to_filename(current_filename, string_to_append)
47
+ extension = File.extname current_filename
48
+ name = File.basename current_filename, extension
49
+ path = File.dirname current_filename
50
+
51
+ "#{path}/#{name}-#{string_to_append}#{extension}"
52
+ end
53
+
54
+ def self.update_report_with_diff(report_file, key_fields=[0])
55
+ old_filename = self.append_to_filename(report_file, 'old')
56
+ new_filename = self.append_to_filename(report_file, 'new')
57
+
58
+ # Report is 'new' file for purpose of diff
59
+ FileUtils.mv(report_file, new_filename)
60
+
61
+ # If the old file doesn't exist, we can just add the status column
62
+ if File.exists?(old_filename)
63
+ self.create_csv_diff(old_filename,
64
+ new_filename,
65
+ report_file,
66
+ key_fields)
67
+ else
68
+ self.overwrite_existing_report(new_filename, report_file)
69
+ end
70
+
71
+ # 'new' file becomes the basis of comparison next time
72
+ FileUtils.mv(new_filename, old_filename, :force => true)
73
+ end
74
+
75
+ # Instead of diffing, append 'status' column to an existing file
76
+ def self.overwrite_existing_report(source_file, target_file)
77
+ temp = Tempfile.new("#{File.basename source_file}tmp")
78
+
79
+ #TODO: Don't do the column_written check every time
80
+
81
+ CSV.open(temp, 'w') do |temp_csv|
82
+ column_written = false
83
+ new_column_value = ['status']
84
+ CSV.foreach(source_file) do |orig|
85
+ temp_csv << (orig + new_column_value)
86
+
87
+ unless column_written
88
+ new_column_value = ['new']
89
+ column_written = true
90
+ end
91
+ end
92
+ end
93
+
94
+ FileUtils.mv(temp, target_file, :force => true)
95
+ end
96
+
97
+ def self.create_csv_diff(old_file, new_file, target_file, key_fields=[0])
98
+ diff = CSVDiff.new(old_file,
99
+ new_file,
100
+ ignore_moves: true,
101
+ key_fields: key_fields
102
+ )
103
+
104
+ columns = self.get_columns(new_file)
105
+
106
+ CSV.open(target_file, 'wb') do |csv|
107
+ csv << (columns+['status'])
108
+
109
+ diff.deletes.each_value { |v| csv << get_delete(v, columns) }
110
+ diff.adds.each_value { |v| csv << get_add(v, columns) }
111
+
112
+ if(key_fields.count == 1)
113
+ # If only a single key field, we don't need the old values
114
+ # Just grab the row and let ServiceNow coalesce and update the row
115
+ update_rows = diff.updates.each_value.map { |u| u.row }
116
+ update_rows = update_rows.map(&:to_i).sort
117
+ update_row_num = update_rows.shift
118
+ current_row_num = 0
119
+
120
+ CSV.foreach(new_file, headers: true) do |new_csv|
121
+ current_row_num = current_row_num + 1
122
+ next unless current_row_num == update_row_num
123
+
124
+ csv << new_csv.push('new')
125
+ update_row_num = update_rows.shift
126
+ break if update_row_num == nil
127
+ end
128
+ elsif(key_fields.count == 2)
129
+ # Multiple key fields result in row "updates"
130
+ diff.updates.each_value do |v|
131
+ csv << self.update_to_old(v, columns)
132
+ csv << self.update_to_new(v, columns)
133
+ end
134
+ else
135
+ raise "Received #{key_fields.count} key fields. Only 1/2 supported."
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
@@ -12,6 +12,9 @@ module NexposeServiceNow
12
12
  DAG_TIMESTAMP_FILE = 'last_scan_data_dag.csv'
13
13
  NEW_DAG_TIMESTAMP_FILE = 'new_dag_timestamp.csv'
14
14
 
15
+ ASSET_GROUP_FILE = 'Nexpose-ServiceNow-asset_groups-old.csv'
16
+ DIFFERENTIAL_FILE_REGEX = 'Nexpose-ServiceNow-*-old.csv'
17
+
15
18
  SITE_IDENTIFIER = 'site_id'
16
19
  SITE_DELTA_VALUE = 'last_scan_id'
17
20
  SITE_BASE_VALUE = 0
@@ -358,18 +361,15 @@ module NexposeServiceNow
358
361
  log_and_print 'Last vuln data updated.'
359
362
  end
360
363
 
361
- def remove_local_file(filename)
364
+ def remove_local_file(filename, action)
362
365
  unless File.exist? filename
363
366
  log_and_error 'Can\'t remove file.'
364
367
  log_and_error "File #{filename} cannot be located."
365
368
  exit -1
366
369
  end
367
370
 
368
- new_name = "#{filename}.#{Time.new.strftime('%Y-%m-%d.%H:%M:%S')}"
369
371
  begin
370
- # Delete existing file with same name
371
- File.delete new_name if File.exist? new_name
372
- File.rename(filename, new_name)
372
+ self.send("by_#{action}", filename)
373
373
  rescue Exception => e
374
374
  log_and_error "Error removing file:\n#{e}"
375
375
  exit -1
@@ -378,13 +378,33 @@ module NexposeServiceNow
378
378
  log_and_print "File #{filename} removed"
379
379
  end
380
380
 
381
+ def self.by_rename(filename)
382
+ new_name = "#{filename}.#{Time.new.strftime('%Y-%m-%d.%H:%M:%S')}"
383
+ File.delete new_name if File.exist? new_name
384
+ File.rename(filename, new_name)
385
+ end
386
+
387
+ def self.by_delete(filename)
388
+ File.delete filename
389
+ end
390
+
381
391
  def remove_last_scan_data
382
- remove_local_file @local_file
392
+ remove_local_file @local_file, 'rename'
393
+ remove_local_file @remote_file, 'delete'
383
394
  end
384
395
 
385
396
  def remove_last_vuln_data
386
- remove_local_file @timestamp_file
397
+ remove_local_file @timestamp_file, 'rename'
398
+ remove_local_file @prev_timestamp_file, 'delete'
387
399
  end
388
400
 
401
+ def remove_last_diff_comparison_data(output_dir)
402
+ local_path = File.expand_path(output_dir)
403
+ remove_local_file (local_path + ASSET_GROUP_FILE), 'rename'
404
+
405
+ Dir[local_path + DIFFERENTIAL_FILE_REGEX].each do |file|
406
+ remove_local_file file, 'delete'
407
+ end
408
+ end
389
409
  end
390
410
  end
@@ -2,6 +2,7 @@ require 'nexpose'
2
2
  require 'fileutils'
3
3
  require_relative './queries'
4
4
  require_relative './nx_logger'
5
+ require_relative './csv_compare'
5
6
 
6
7
  module NexposeServiceNow
7
8
  class NexposeHelper
@@ -77,7 +78,14 @@ module NexposeServiceNow
77
78
  report_id = generate_config(query, report_name, [id], id_type, min_cvss)
78
79
 
79
80
  run_report(report_id)
80
- reports << save_report(report_name, report_id, output_dir)
81
+ local_report_name = save_report(report_name, report_id, output_dir)
82
+ reports << local_report_name
83
+
84
+ if Queries.csv_diff_required?(query_name)
85
+ @log.log_message "Calculating diff for #{local_report_name}..."
86
+ CsvCompare.update_report_with_diff(local_report_name,
87
+ Queries.query_keys(query_name))
88
+ end
81
89
  end
82
90
 
83
91
  reports
@@ -14,7 +14,7 @@ module NexposeServiceNow
14
14
  pci_status,
15
15
  pci_adjusted_cvss_score as PCI_Severity,
16
16
  title as Summary,
17
- description as Threat,
17
+ proofAsText(description) as Threat,
18
18
  ROUND(riskscore::numeric, 2) as Riskscore,
19
19
  cvss_vector,
20
20
  ROUND(cvss_impact_score::numeric, 2) as Impact_Score,
@@ -36,7 +36,7 @@ module NexposeServiceNow
36
36
  THEN 1
37
37
  ELSE 0
38
38
  END AS bit) as Malware_Kits,
39
- sol.solutions as Solution
39
+ array_to_string(sol.solutions, ',', '') as Solution
40
40
 
41
41
  FROM
42
42
  dim_vulnerability
@@ -68,14 +68,8 @@ module NexposeServiceNow
68
68
  GROUP BY dvr.vulnerability_id) ref USING (vulnerability_id)
69
69
 
70
70
  LEFT OUTER JOIN(SELECT vulnerability_id,
71
- string_agg(concat('Fix: ' || fix,
72
- 'Solution type: ' || solution_type,
73
- 'URL: ' || url,
74
- 'Estimate: ' || estimate,
75
- 'Applies To: ' || applies_to,
76
- 'Additional Data: ' || additional_data), '\n') as solutions
77
- FROM dim_solution
78
- JOIN dim_vulnerability_solution USING (solution_id)
71
+ array_agg(solution_id) as solutions
72
+ FROM dim_vulnerability_solution
79
73
  GROUP BY vulnerability_id) sol USING (vulnerability_id)
80
74
  WHERE date_modified >= '#{options[:vuln_query_date]}'"
81
75
  end
@@ -113,10 +107,12 @@ module NexposeServiceNow
113
107
  dim_operating_system.description as Operating_System,
114
108
  fact_asset.scan_finished as Most_Recent_Discovery,
115
109
  dim_asset.asset_id as Nexpose_ID,
116
- fact_asset.pci_status
110
+ fact_asset.pci_status,
111
+ concat(fact_asset.aggregated_credential_status_id, ' - ', aggregated_credential_status_description) as Credential_Status
117
112
 
118
113
  FROM dim_asset
119
114
  JOIN fact_asset USING (asset_id)
115
+ JOIN dim_aggregated_credential_status USING (aggregated_credential_status_id)
120
116
  LEFT OUTER JOIN dim_operating_system on dim_asset.operating_system_id = dim_operating_system.operating_system_id
121
117
  LEFT OUTER JOIN dim_host_type USING (host_type_id)"
122
118
  end
@@ -129,16 +125,44 @@ module NexposeServiceNow
129
125
  WHERE scan_id = lastScan(asset_id)"
130
126
  end
131
127
 
128
+ def self.service_definition(options={})
129
+ "SELECT DISTINCT on(dsf.name, ds.name, dp.name, port)
130
+ dsf.name, ds.name as service_name, dp.name as protocol, port
131
+
132
+ FROM (SELECT service_id, protocol_id, port, service_fingerprint_id
133
+ FROM fact_asset_scan_service
134
+ GROUP BY asset_id, service_id, protocol_id, port, service_fingerprint_id
135
+ HAVING min(scan_id) > #{options[:delta]} and max(scan_id) = lastScan(asset_id)) fass
136
+ JOIN dim_service ds USING (service_id)
137
+ JOIN dim_protocol dp USING (protocol_id)
138
+ JOIN dim_service_fingerprint dsf USING (service_fingerprint_id)"
139
+ end
140
+
141
+ # When a service only appears in either the old or latest import,
142
+ # then it needs deleted or inserted, respectively.
132
143
  def self.service_instance(options={})
133
- 'SELECT ds.Service_Name, asset_id as Nexpose_ID, port, dp.protocol, dsf.name
144
+ "SELECT asset_id, dsf.name as name,
145
+ ds.name as service_name,
146
+ dp.name as protocol, port,
147
+ CASE
148
+ WHEN scan_id = #{options[:delta]}
149
+ THEN 'old'
150
+ WHEN scan_id = lastScan(asset_id)
151
+ THEN 'new'
152
+ ELSE 'current'
153
+ END as status
154
+
155
+ FROM (SELECT asset_id, service_id, protocol_id, port, min(scan_id) as scan_id, service_fingerprint_id
134
156
  FROM fact_asset_scan_service
135
- LEFT OUTER JOIN (SELECT service_id, name as service_name FROM dim_service) ds USING (service_id)
136
- LEFT OUTER JOIN (SELECT service_fingerprint_id, name FROM dim_service_fingerprint) dsf USING (service_fingerprint_id)
137
- LEFT OUTER JOIN (SELECT protocol_id, name as protocol FROM dim_protocol) dp USING (protocol_id)
138
- WHERE scan_id = lastScan(asset_id)'
157
+ WHERE scan_id = lastScan(asset_id) OR scan_id = #{options[:delta]}
158
+ GROUP BY asset_id, service_id, protocol_id, port, service_fingerprint_id
159
+ HAVING min(scan_id) = max(scan_id)) fass
160
+ JOIN dim_service ds USING (service_id)
161
+ JOIN dim_protocol dp USING (protocol_id)
162
+ JOIN dim_service_fingerprint dsf USING (service_fingerprint_id)
163
+ GROUP BY asset_id, dsf.name, ds.name, dp.name, port, scan_id"
139
164
  end
140
165
 
141
-
142
166
  # Need to wipe table each time
143
167
  def self.group_accounts(options={})
144
168
  'SELECT asset_id as Nexpose_ID, daga.name as Group_Account_Name
@@ -155,12 +179,13 @@ module NexposeServiceNow
155
179
  JOIN dim_asset_user_account daua USING (asset_id)'
156
180
  end
157
181
 
158
- # Need to wipe table each time
159
182
  def self.asset_groups(options={})
160
- 'SELECT asset_id as Nexpose_ID, dag.name as Asset_Group_Name,
161
- dag.dynamic_membership, dag.description
162
- FROM dim_asset_group_asset daga
163
- JOIN dim_asset_group dag on daga.asset_group_id = dag.asset_group_id'
183
+ 'SELECT asset_group_id, name as asset_group_name, description, dynamic_membership
184
+ FROM dim_asset_group'
185
+ end
186
+
187
+ def self.asset_group_memberships(options={})
188
+ 'select * from dim_asset_group_asset'
164
189
  end
165
190
 
166
191
  # Need to wipe table each time
@@ -243,8 +268,6 @@ module NexposeServiceNow
243
268
  end
244
269
 
245
270
  def self.vulnerable_new_items(options={})
246
- # self.send("vulnerable_new_items_per_#{options[:id_type]}", options)
247
-
248
271
  standard_filter = if options[:id_type] == 'site'
249
272
  "MIN(fasv.scan_id) > #{options[:delta]}"
250
273
  else
@@ -256,40 +279,51 @@ module NexposeServiceNow
256
279
  cvss_filter = self.generate_cvss_filter(options[:filters][:cvss])
257
280
 
258
281
  "SELECT
259
- CAST(subq.asset_id as text) Configuration_Item,
260
- TRUE as Active,
261
- concat('R7_', subq.vulnerability_id) as Vulnerability,
262
- fasva.first_discovered as First_Found,
263
- fasva.most_recently_discovered as Last_Found,
264
- subq.vulnerability_instances as Times_Found,
265
- subq.ip_address as IP_Address,
266
- favi.port as Port,
267
- coalesce(NULLIF(favi.name,''), 'None') as Protocol
268
-
269
- FROM (
282
+ CAST(subq.asset_id as text) Configuration_Item,
283
+ TRUE as Active,
284
+ concat('R7_', subq.vulnerability_id) as Vulnerability,
285
+ fasva.first_discovered as First_Found,
286
+ fasva.most_recently_discovered as Last_Found,
287
+ subq.vulnerability_instances as Times_Found,
288
+ subq.ip_address as IP_Address,
289
+ coalesce(NULLIF(CONCAT('\"', favi.proof ,'\"'), '\"\"'), 'None') as Proof,
290
+ favi.stat_desc as Status,
291
+ array_to_string(favi.ports, ' ', '') as Ports,
292
+ coalesce(NULLIF(concat(favi.pro_desc, ' (', favi.name, ')') ,'N/A ()'), 'None') as Protocol,
293
+ array_to_string(favi.solution_ids, ',', '') as Solutions
294
+ FROM (
270
295
  SELECT fasv.asset_id, fasv.vulnerability_id, vulnerability_instances,
271
296
  MIN(fasv.scan_id) as first_found, MAX(fasv.scan_id) as latest_found,
272
297
  s.current_scan, s.host_name, s.ip_address
273
- FROM fact_asset_scan_vulnerability_finding fasv
274
-
275
- #{cve_filter}
276
- #{cvss_filter}
277
-
278
- JOIN (
279
- SELECT asset_id, host_name, ip_address, lastScan(asset_id) AS current_scan FROM dim_asset
280
- ) s ON s.asset_id = fasv.asset_id
281
- GROUP BY fasv.asset_id, fasv.vulnerability_id, s.current_scan, s.host_name, s.ip_address, vulnerability_instances
282
- HAVING MAX(fasv.scan_id)=current_scan AND #{standard_filter}
283
- ) subq
284
-
285
- JOIN (select asset_id, vulnerability_id,
286
- first_discovered, most_recently_discovered
287
- from fact_asset_vulnerability_age
288
- #{date_filter}) fasva USING (asset_id, vulnerability_id)
289
-
290
- JOIN (select DISTINCT on(asset_id, vulnerability_id) asset_id, scan_id, vulnerability_id, port, dp.name
291
- from fact_asset_vulnerability_instance
292
- inner join dim_protocol dp USING (protocol_id)) favi ON favi.asset_id = subq.asset_id AND favi.scan_id = subq.current_scan AND favi.vulnerability_id = subq.vulnerability_id
298
+ FROM fact_asset_scan_vulnerability_finding fasv
299
+
300
+ #{cve_filter}
301
+ #{cvss_filter}
302
+
303
+ JOIN (SELECT asset_id, host_name, ip_address, lastScan(asset_id) AS current_scan
304
+ FROM dim_asset
305
+ ) s ON s.asset_id = fasv.asset_id
306
+ GROUP BY fasv.asset_id, fasv.vulnerability_id, s.current_scan, s.host_name, s.ip_address, vulnerability_instances
307
+ HAVING MAX(fasv.scan_id)=current_scan AND #{standard_filter}
308
+ ) subq
309
+
310
+ JOIN (SELECT asset_id, vulnerability_id,
311
+ first_discovered, most_recently_discovered
312
+ FROM fact_asset_vulnerability_age
313
+ #{date_filter}) fasva USING (asset_id, vulnerability_id)
314
+
315
+ JOIN (SELECT DISTINCT on(asset_id, vulnerability_id, scan_id)
316
+ asset_id, scan_id, vulnerability_id,
317
+ proofAsText(proof) as proof, vs.description as stat_desc,
318
+ array_agg(DISTINCT port) as ports, dp.name,
319
+ dp.description as pro_desc,
320
+ array_agg(DISTINCT dvs.solution_id) as solution_ids
321
+ FROM fact_asset_vulnerability_instance
322
+ INNER JOIN dim_protocol dp USING (protocol_id)
323
+ INNER JOIN dim_vulnerability_status vs using (status_id)
324
+ LEFT JOIN dim_asset_vulnerability_solution dvs USING (asset_id, vulnerability_id)
325
+ GROUP BY asset_id, scan_id, vulnerability_id, proof, stat_desc, port, dp.name, pro_desc
326
+ ) favi ON favi.asset_id = subq.asset_id AND favi.scan_id = subq.current_scan AND favi.vulnerability_id = subq.vulnerability_id
293
327
  ORDER BY fasva.asset_id, vulnerability"
294
328
  end
295
329
 
@@ -342,6 +376,53 @@ module NexposeServiceNow
342
376
 
343
377
  end
344
378
 
379
+ def self.vulnerability_solutions(options={})
380
+ "SELECT DISTINCT (solution_id)
381
+ solution_id,
382
+ nexpose_id,
383
+ coalesce(NULLIF(CONCAT('\"', proofAsText(fix),'\"'), '\"\"'), 'None') as fix,
384
+ estimate,
385
+ summary,
386
+ solution_type,
387
+ applies_to,
388
+ url,
389
+ coalesce(NULLIF(CONCAT('\"',proofAsText(additional_data),'\"'), '\"\"'), 'None') as additional_data,
390
+ array_to_string(req_solutions, ',', '') as required_solutions,
391
+ array_to_string(super_solutions, ',', '') as superceding_solutions
392
+ FROM dim_solution
393
+ RIGHT OUTER JOIN (
394
+ SELECT DISTINCT (solution_id) solution_id
395
+ FROM (
396
+ SELECT solution_id, vulnerability_id, date_modified
397
+ FROM dim_vulnerability
398
+ LEFT JOIN dim_vulnerability_solution idvs USING (vulnerability_id)
399
+ ) dvs
400
+ WHERE date_modified >= '#{options[:vuln_query_date]}'
401
+ UNION
402
+ SELECT DISTINCT (solution_id) solution_id
403
+ FROM dim_solution
404
+ LEFT JOIN (
405
+ SELECT solution_id, vulnerability_id, date_modified
406
+ FROM dim_vulnerability
407
+ LEFT JOIN dim_vulnerability_solution idvs USING (vulnerability_id)
408
+ ) ndvs USING (solution_id)
409
+ WHERE vulnerability_id IS NULL
410
+ ) dvs USING (solution_id)
411
+ LEFT JOIN (
412
+ SELECT DISTINCT (solution_id) solution_id,
413
+ array_agg(required_solution_id) as req_solutions
414
+ FROM dim_solution_prerequisite
415
+ GROUP BY solution_id
416
+ ) dsp USING (solution_id)
417
+ JOIN (
418
+ SELECT DISTINCT (solution_id) solution_id,
419
+ array_agg(superceding_solution_id) as super_solutions
420
+ FROM dim_solution_highest_supercedence
421
+ GROUP BY solution_id
422
+ ) dshs USING (solution_id)
423
+ ORDER BY solution_id"
424
+ end
425
+
345
426
  def self.latest_scans(options={})
346
427
  'SELECT ds.site_id, ds.last_scan_id, dsc.finished
347
428
  FROM dim_site ds
@@ -350,8 +431,24 @@ module NexposeServiceNow
350
431
 
351
432
  def self.multiple_reports?(query_name)
352
433
  single_queries = %w(vulnerabilities vulnerability_category
353
- vulnerability_references latest_scans)
434
+ asset_groups latest_scans
435
+ vulnerability_solutions vulnerability_references
436
+ )
354
437
  return !(single_queries.include? query_name.to_s)
355
438
  end
439
+
440
+ def self.csv_diff_required?(query_name)
441
+ diff_required = %w(asset_groups asset_group_memberships)
442
+ return (diff_required.include? query_name.to_s)
443
+ end
444
+
445
+ # TODO: Refactor this.
446
+ def self.query_keys(query_name)
447
+ if query_name.to_s == 'asset_groups'
448
+ [0]
449
+ else
450
+ [0,1]
451
+ end
452
+ end
356
453
  end
357
454
  end
@@ -1,5 +1,5 @@
1
1
  module NexposeServiceNow
2
- VERSION = '0.5.1'
2
+ VERSION = '0.6.0'
3
3
  VENDOR = 'ServiceNow'
4
4
  PRODUCT = 'CMDB'
5
5
  end
@@ -28,7 +28,8 @@ Gem::Specification.new do |spec|
28
28
  spec.bindir = "bin"
29
29
  spec.require_paths = ["lib"]
30
30
 
31
- spec.add_development_dependency "bundler", "~> 1.11"
32
- spec.add_development_dependency "rake", "~> 10.0"
31
+ spec.add_development_dependency 'bundler', '~> 1.11'
32
+ spec.add_development_dependency 'rake', '~> 10.0'
33
33
  spec.add_dependency 'nexpose', '~> 3.2'
34
+ spec.add_dependency 'csv-diff', '~> 0.3.3'
34
35
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nexpose_servicenow
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.1
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Valente
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-04-26 00:00:00.000000000 Z
11
+ date: 2017-08-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '3.2'
55
+ - !ruby/object:Gem::Dependency
56
+ name: csv-diff
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 0.3.3
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 0.3.3
55
69
  description: Provides an interface to Nexpose for the Rapid7 ServiceNow MarketPlace
56
70
  application.
57
71
  email:
@@ -70,6 +84,7 @@ files:
70
84
  - lib/nexpose_servicenow.rb
71
85
  - lib/nexpose_servicenow/arg_parser.rb
72
86
  - lib/nexpose_servicenow/chunker.rb
87
+ - lib/nexpose_servicenow/csv_compare.rb
73
88
  - lib/nexpose_servicenow/historical_data.rb
74
89
  - lib/nexpose_servicenow/nexpose_helper.rb
75
90
  - lib/nexpose_servicenow/nx_logger.rb