nexpose_servicenow 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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