nexpose_servicenow 0.7.3 → 0.8.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 +4 -4
- data/README.md +51 -15
- data/bin/nexpose_servicenow +10 -2
- data/lib/nexpose_servicenow.rb +1 -174
- data/lib/nexpose_servicenow/version.rb +14 -1
- data/nexpose_servicenow.gemspec +2 -4
- metadata +13 -59
- data/lib/nexpose_servicenow/arg_parser.rb +0 -283
- data/lib/nexpose_servicenow/chunker.rb +0 -109
- data/lib/nexpose_servicenow/csv_compare.rb +0 -177
- data/lib/nexpose_servicenow/helpers/connection_helper.rb +0 -84
- data/lib/nexpose_servicenow/helpers/data_warehouse_helper.rb +0 -140
- data/lib/nexpose_servicenow/helpers/nexpose_console_helper.rb +0 -164
- data/lib/nexpose_servicenow/historical_data.rb +0 -102
- data/lib/nexpose_servicenow/nx_logger.rb +0 -166
- data/lib/nexpose_servicenow/queries/nexpose_queries.rb +0 -459
- data/lib/nexpose_servicenow/queries/queries_base.rb +0 -25
- data/lib/nexpose_servicenow/queries/warehouse_queries.rb +0 -341
@@ -1,109 +0,0 @@
|
|
1
|
-
#TODO: Check if using site_id is OK. If the object is changed, the code on SN will likely need changed.
|
2
|
-
|
3
|
-
module NexposeServiceNow
|
4
|
-
class Chunker
|
5
|
-
def initialize(report_details, row_limit)
|
6
|
-
@row_limit = row_limit
|
7
|
-
@size_limit = 4_500_000
|
8
|
-
@report_details = report_details
|
9
|
-
@header = get_header
|
10
|
-
|
11
|
-
setup_logging
|
12
|
-
end
|
13
|
-
|
14
|
-
def setup_logging
|
15
|
-
@log = NexposeServiceNow::NxLogger.instance
|
16
|
-
@log.log_message("Chunk File Limit:\t#{@size_limit}MB")
|
17
|
-
@log.log_message("Chunk Row Limit:\t#{@row_limit}")
|
18
|
-
end
|
19
|
-
|
20
|
-
# Grab the header from the first file
|
21
|
-
def get_header
|
22
|
-
file = File.open(@report_details.first[:report_name], 'r')
|
23
|
-
header = file.readline
|
24
|
-
file.close
|
25
|
-
|
26
|
-
header
|
27
|
-
end
|
28
|
-
|
29
|
-
def preprocess
|
30
|
-
all_chunks = []
|
31
|
-
@report_details.each do |report|
|
32
|
-
@log.log_message("Dividing file #{report[:report_name]} into chunks.")
|
33
|
-
chunks = process_file(report[:report_name], report[:id])
|
34
|
-
all_chunks.concat chunks
|
35
|
-
end
|
36
|
-
|
37
|
-
@log.log_message("Files divided into #{all_chunks.count} chunks")
|
38
|
-
|
39
|
-
puts all_chunks.to_json
|
40
|
-
end
|
41
|
-
|
42
|
-
def process_file(file_path, site_id=nil)
|
43
|
-
relative_size_limit = @size_limit - @header.bytesize
|
44
|
-
chunk = { site_id: site_id,
|
45
|
-
start: @header.bytesize,
|
46
|
-
length: 0,
|
47
|
-
row_count: 0 }
|
48
|
-
|
49
|
-
chunks = []
|
50
|
-
csv_file = CSV.open(file_path, 'r', headers: true)
|
51
|
-
while(true)
|
52
|
-
position = csv_file.pos
|
53
|
-
line = csv_file.shift
|
54
|
-
row_length = line.to_s.bytesize
|
55
|
-
|
56
|
-
if line.nil?
|
57
|
-
chunks << chunk
|
58
|
-
break
|
59
|
-
elsif chunk[:length]+row_length < relative_size_limit &&
|
60
|
-
chunk[:row_count] + 1 < @row_limit
|
61
|
-
chunk[:length] += row_length
|
62
|
-
chunk[:row_count] += 1
|
63
|
-
else
|
64
|
-
chunks << chunk
|
65
|
-
|
66
|
-
#Initialise chunk with this row information
|
67
|
-
chunk = { site_id: site_id,
|
68
|
-
start: position,
|
69
|
-
length: row_length,
|
70
|
-
row_count: 1 }
|
71
|
-
end
|
72
|
-
end
|
73
|
-
csv_file.close
|
74
|
-
|
75
|
-
#Should we include the row count?
|
76
|
-
chunks.each do |c|
|
77
|
-
c.delete :row_count
|
78
|
-
|
79
|
-
#Should we do this...?
|
80
|
-
c.delete :site_id if c[:site_id].nil? || c[:site_id] == -1
|
81
|
-
end
|
82
|
-
|
83
|
-
chunks
|
84
|
-
end
|
85
|
-
|
86
|
-
def get_file(site_id=nil)
|
87
|
-
# -1 indicates a single query report
|
88
|
-
return @report_details.first[:report_name] if site_id.to_i <= 0
|
89
|
-
|
90
|
-
report = @report_details.find { |r| r[:id].to_s == site_id.to_s }
|
91
|
-
report[:report_name]
|
92
|
-
end
|
93
|
-
|
94
|
-
def read_chunk(start, length, site_id=nil)
|
95
|
-
file_path = get_file(site_id)
|
96
|
-
msg = "Returning chunk. Start: #{start}, " \
|
97
|
-
"Length: #{length}, File: #{file_path}"
|
98
|
-
@log.log_message(msg)
|
99
|
-
|
100
|
-
#If the header isn't in the chunk, prepend it
|
101
|
-
header = start == 0 ? '' : @header
|
102
|
-
|
103
|
-
file = File.open(file_path, 'rb')
|
104
|
-
file.seek(start)
|
105
|
-
puts header + file.read(length)
|
106
|
-
file.close
|
107
|
-
end
|
108
|
-
end
|
109
|
-
end
|
@@ -1,177 +0,0 @@
|
|
1
|
-
require 'fileutils'
|
2
|
-
require 'tempfile'
|
3
|
-
require 'csv-diff'
|
4
|
-
require 'csv'
|
5
|
-
|
6
|
-
|
7
|
-
class CSVDiff
|
8
|
-
class CSVSource
|
9
|
-
def find_field_indexes(key_fields, field_names)
|
10
|
-
key_fields.map do |field|
|
11
|
-
if field.is_a?(Integer)
|
12
|
-
field
|
13
|
-
else
|
14
|
-
field_names.index{ |field_name| field.to_s.downcase == field_name.downcase } or
|
15
|
-
raise ArgumentError, "Could not locate field '#{field}' in source field names: #{
|
16
|
-
field_names.join(', ')}"
|
17
|
-
end
|
18
|
-
end
|
19
|
-
end
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
module NexposeServiceNow
|
24
|
-
class CsvCompare
|
25
|
-
def self.get_columns(csv_file)
|
26
|
-
columns = ''
|
27
|
-
File::open(csv_file,'r') do |f|
|
28
|
-
columns = f.readline.rstrip
|
29
|
-
end
|
30
|
-
|
31
|
-
columns.split(',')
|
32
|
-
end
|
33
|
-
|
34
|
-
def self.get_row(value, columns, status)
|
35
|
-
columns.map { |c| value.fields[c] }.push(status)
|
36
|
-
end
|
37
|
-
|
38
|
-
def self.get_delete(value, columns)
|
39
|
-
self.get_row(value, columns, 'old')
|
40
|
-
end
|
41
|
-
|
42
|
-
def self.get_add(value, columns)
|
43
|
-
self.get_row(value, columns, 'new')
|
44
|
-
end
|
45
|
-
|
46
|
-
def self.update_to_old(value, columns)
|
47
|
-
self.update_to_row(value, columns, 0, 'old')
|
48
|
-
end
|
49
|
-
|
50
|
-
def self.update_to_new(value, columns)
|
51
|
-
self.update_to_row(value, columns, 1, 'new')
|
52
|
-
end
|
53
|
-
|
54
|
-
def self.update_to_row(value, columns, index, status)
|
55
|
-
row = []
|
56
|
-
columns.each do |c|
|
57
|
-
val = value.fields[c]
|
58
|
-
row << (val.kind_of?(Array) ? val[index] : val)
|
59
|
-
end
|
60
|
-
row.push(status)
|
61
|
-
end
|
62
|
-
|
63
|
-
def self.append_to_filename(current_filename, string_to_append)
|
64
|
-
extension = File.extname current_filename
|
65
|
-
name = File.basename current_filename, extension
|
66
|
-
path = File.dirname current_filename
|
67
|
-
|
68
|
-
"#{path}/#{name}-#{string_to_append}#{extension}"
|
69
|
-
end
|
70
|
-
|
71
|
-
def self.update_report_with_diff(report_file, key_fields=[0])
|
72
|
-
old_filename = self.append_to_filename(report_file, 'old')
|
73
|
-
new_filename = self.append_to_filename(report_file, 'new')
|
74
|
-
|
75
|
-
# Report is 'new' file for purpose of diff
|
76
|
-
FileUtils.mv(report_file, new_filename)
|
77
|
-
|
78
|
-
# If the old file doesn't exist, we can just add the status column
|
79
|
-
if File.exists?(old_filename)
|
80
|
-
self.create_csv_diff(old_filename,
|
81
|
-
new_filename,
|
82
|
-
report_file,
|
83
|
-
key_fields)
|
84
|
-
else
|
85
|
-
self.overwrite_existing_report(new_filename, report_file)
|
86
|
-
end
|
87
|
-
|
88
|
-
# 'new' file becomes the basis of comparison next time
|
89
|
-
FileUtils.mv(new_filename, old_filename, :force => true)
|
90
|
-
end
|
91
|
-
|
92
|
-
# Instead of diffing, append 'status' column to an existing file
|
93
|
-
def self.overwrite_existing_report(source_file, target_file)
|
94
|
-
temp = Tempfile.new("#{File.basename source_file}tmp")
|
95
|
-
|
96
|
-
#TODO: Don't do the column_written check every time
|
97
|
-
|
98
|
-
CSV.open(temp, 'w') do |temp_csv|
|
99
|
-
column_written = false
|
100
|
-
new_column_value = ['status']
|
101
|
-
CSV.foreach(source_file) do |orig|
|
102
|
-
temp_csv << (orig + new_column_value)
|
103
|
-
|
104
|
-
unless column_written
|
105
|
-
new_column_value = ['new']
|
106
|
-
column_written = true
|
107
|
-
end
|
108
|
-
end
|
109
|
-
end
|
110
|
-
|
111
|
-
temp.close
|
112
|
-
FileUtils.mv(temp, target_file, :force => true)
|
113
|
-
end
|
114
|
-
|
115
|
-
def self.create_csv_diff(old_file, new_file, target_file, key_fields=[0])
|
116
|
-
old_temp = Tempfile.new("#{File.basename old_file}tmp")
|
117
|
-
new_temp = Tempfile.new("#{File.basename new_file}tmp")
|
118
|
-
|
119
|
-
File.open(old_file) do |f|
|
120
|
-
IO.copy_stream(f, old_temp)
|
121
|
-
end
|
122
|
-
File.open(new_file) do |f|
|
123
|
-
IO.copy_stream(f, new_temp)
|
124
|
-
end
|
125
|
-
|
126
|
-
begin
|
127
|
-
diff = CSVDiff.new(
|
128
|
-
File.expand_path(old_temp),
|
129
|
-
File.expand_path(new_temp),
|
130
|
-
ignore_moves: true,
|
131
|
-
key_fields: key_fields
|
132
|
-
)
|
133
|
-
rescue Exception => e
|
134
|
-
file_name = File.basename target_file
|
135
|
-
raise "Unable to diff file: #{file_name}. \nError received: #{e}"
|
136
|
-
ensure
|
137
|
-
old_temp.close!
|
138
|
-
new_temp.close!
|
139
|
-
end
|
140
|
-
|
141
|
-
columns = self.get_columns(new_file)
|
142
|
-
|
143
|
-
CSV.open(target_file, 'wb') do |csv|
|
144
|
-
csv << (columns+['status'])
|
145
|
-
|
146
|
-
diff.deletes.each_value { |v| csv << get_delete(v, columns) }
|
147
|
-
diff.adds.each_value { |v| csv << get_add(v, columns) }
|
148
|
-
|
149
|
-
if key_fields.count == 1
|
150
|
-
# If only a single key field, we don't need the old values
|
151
|
-
# Just grab the row and let ServiceNow coalesce and update the row
|
152
|
-
update_rows = diff.updates.each_value.map { |u| u.row }
|
153
|
-
update_rows = update_rows.map(&:to_i).sort
|
154
|
-
update_row_num = update_rows.shift
|
155
|
-
current_row_num = 0
|
156
|
-
|
157
|
-
CSV.foreach(new_file, headers: true) do |new_csv|
|
158
|
-
current_row_num = current_row_num + 1
|
159
|
-
next unless current_row_num == update_row_num
|
160
|
-
|
161
|
-
csv << new_csv.push('new')
|
162
|
-
update_row_num = update_rows.shift
|
163
|
-
break if update_row_num == nil
|
164
|
-
end
|
165
|
-
elsif key_fields.count == 2
|
166
|
-
# Multiple key fields result in row "updates"
|
167
|
-
diff.updates.each_value do |v|
|
168
|
-
csv << self.update_to_old(v, columns)
|
169
|
-
csv << self.update_to_new(v, columns)
|
170
|
-
end
|
171
|
-
else
|
172
|
-
raise "Received #{key_fields.count} key fields. Only 1/2 supported."
|
173
|
-
end
|
174
|
-
end
|
175
|
-
end
|
176
|
-
end
|
177
|
-
end
|
@@ -1,84 +0,0 @@
|
|
1
|
-
require_relative '../queries/queries_base'
|
2
|
-
|
3
|
-
module NexposeServiceNow
|
4
|
-
class ConnectionHelper
|
5
|
-
NOT_IMPL = 'Error: Method not implemented.'
|
6
|
-
|
7
|
-
def initialize(url, port, username, password, silo='')
|
8
|
-
@log = NexposeServiceNow::NxLogger.instance
|
9
|
-
@url = url
|
10
|
-
@port = port
|
11
|
-
@username = username
|
12
|
-
@password = password
|
13
|
-
@silo = silo
|
14
|
-
|
15
|
-
@timeout = 21600
|
16
|
-
end
|
17
|
-
|
18
|
-
def self.get_report_names(query_name, ids)
|
19
|
-
if QueriesBase.single_report?(query_name)
|
20
|
-
return [ id: -1, report_name: get_report_name(query_name) ]
|
21
|
-
end
|
22
|
-
|
23
|
-
ids.map do |id|
|
24
|
-
{ id: id.first, report_name: get_report_name(query_name, id.first) }
|
25
|
-
end
|
26
|
-
end
|
27
|
-
|
28
|
-
def self.get_report_name(query_name, id=nil)
|
29
|
-
name = "Nexpose-ServiceNow-#{query_name}"
|
30
|
-
name += "-#{id}" unless QueriesBase.single_report?(query_name) || id.nil?
|
31
|
-
name
|
32
|
-
end
|
33
|
-
|
34
|
-
def create_query_options(query_options, nexpose_id=nil)
|
35
|
-
options = {}
|
36
|
-
options[:vuln_query_date] = query_options[:vuln_query_date]
|
37
|
-
options[:site_id] = nexpose_id
|
38
|
-
options[:id_type] = 'site'
|
39
|
-
options[:filters] = query_options[:filters] || {}
|
40
|
-
options[:cvss_v] = query_options[:cvss_v]
|
41
|
-
|
42
|
-
# Without a nexpose ID, we don't have a specific delta
|
43
|
-
return options if [nil, -1].include? nexpose_id
|
44
|
-
|
45
|
-
if query_options[:delta_values].empty?
|
46
|
-
error_msg = 'No delta values provided. Exiting...'
|
47
|
-
@log.log_error_message error_msg
|
48
|
-
raise error_msg
|
49
|
-
end
|
50
|
-
|
51
|
-
options[:delta] = "#{query_options[:delta_values][nexpose_id] || 0}"
|
52
|
-
|
53
|
-
@log.log_message("Query options: #{options}")
|
54
|
-
|
55
|
-
options
|
56
|
-
end
|
57
|
-
|
58
|
-
def self.get_filepath(report_name, output_dir)
|
59
|
-
path = File.join output_dir, "#{report_name}.csv"
|
60
|
-
File.expand_path path
|
61
|
-
end
|
62
|
-
|
63
|
-
def connect(username, password)
|
64
|
-
raise NOT_IMPL
|
65
|
-
end
|
66
|
-
|
67
|
-
def generate_report(query_name, ids, id_type, output_dir, query_options={})
|
68
|
-
raise NOT_IMPL
|
69
|
-
end
|
70
|
-
|
71
|
-
# Pulls the collection IDs from Nexpose (e.g. asset groups, sites)
|
72
|
-
def collection_ids(collection_type)
|
73
|
-
raise NOT_IMPL
|
74
|
-
end
|
75
|
-
|
76
|
-
def save_report(report_name, report_id, output_dir)
|
77
|
-
raise NOT_IMPL
|
78
|
-
end
|
79
|
-
|
80
|
-
def get_cvss_version_strings(use_v3)
|
81
|
-
raise NOT_IMPL
|
82
|
-
end
|
83
|
-
end
|
84
|
-
end
|
@@ -1,140 +0,0 @@
|
|
1
|
-
require 'pg'
|
2
|
-
require_relative './connection_helper'
|
3
|
-
require_relative '../queries/warehouse_queries'
|
4
|
-
|
5
|
-
module NexposeServiceNow
|
6
|
-
class DataWarehouseHelper < ConnectionHelper
|
7
|
-
|
8
|
-
SSL_MODE = 'prefer'
|
9
|
-
GRANULARITY = 500
|
10
|
-
CHUNK_LIMIT = 4_500_000
|
11
|
-
|
12
|
-
def initialize(url, port, username, password, silo='')
|
13
|
-
super(url, port, username, password, silo)
|
14
|
-
end
|
15
|
-
|
16
|
-
def connect
|
17
|
-
@log.log_message 'Creating DWH connection'
|
18
|
-
PG::Connection.open(:host => @url,
|
19
|
-
:dbname => @silo,
|
20
|
-
:port => @port,
|
21
|
-
:user => @username,
|
22
|
-
:password => @password,
|
23
|
-
:sslmode => SSL_MODE)
|
24
|
-
end
|
25
|
-
|
26
|
-
def generate_report(query_name, ids, id_type, output_dir, query_options={})
|
27
|
-
output_dir = File.expand_path(output_dir.to_s)
|
28
|
-
|
29
|
-
#A single report doesn't use site filters
|
30
|
-
ids = [-1] if WarehouseQueries.single_report?(query_name)
|
31
|
-
|
32
|
-
query_options[:cvss_v] = get_cvss_version_strings(query_options[:cvss_v3])
|
33
|
-
|
34
|
-
page_size = query_options[:page_size]
|
35
|
-
row_limit = query_options[:row_limit]
|
36
|
-
|
37
|
-
chunks = []
|
38
|
-
|
39
|
-
base_name = "query_#{query_name}"
|
40
|
-
|
41
|
-
ids.each do |id|
|
42
|
-
delta_options = create_query_options(query_options, id)
|
43
|
-
query = WarehouseQueries.send(query_name, delta_options)
|
44
|
-
|
45
|
-
# Open the CSV file to write as pages are retrieved
|
46
|
-
report_name = self.class.get_report_name(query_name, id)
|
47
|
-
@log.log_message "Running query for #{report_name}"
|
48
|
-
|
49
|
-
local_file_name = self.class.get_filepath(report_name, output_dir)
|
50
|
-
csvFile = File.open(local_file_name, 'wb')
|
51
|
-
|
52
|
-
conn = connect
|
53
|
-
conn.transaction do
|
54
|
-
table_name = "query_#{query_name}"
|
55
|
-
table_name = "#{base_name}_#{id}" if id && id.to_i > 0
|
56
|
-
|
57
|
-
@log.log_message "Creating cursor: #{table_name}"
|
58
|
-
conn.exec("DECLARE #{table_name} CURSOR FOR #{query}")
|
59
|
-
res = conn.exec("FETCH #{page_size} FROM #{table_name}")
|
60
|
-
|
61
|
-
# Headers
|
62
|
-
headers = res.fields.join(',')
|
63
|
-
csvFile.puts(headers)
|
64
|
-
|
65
|
-
# Declare the initial chunk
|
66
|
-
chunk = { start: csvFile.pos, length: 0, row_count: 0 }
|
67
|
-
chunk[:site_id] = id unless id.nil? || id == -1
|
68
|
-
|
69
|
-
# Should we overwrite 'res' to release the memory?
|
70
|
-
all_lines = res.values.map { |r| r.join(',') }
|
71
|
-
|
72
|
-
# Table declared, so keep reading pages until it's consumed
|
73
|
-
data_left = true
|
74
|
-
while(data_left)
|
75
|
-
# Lift out a number of lines in a chunk
|
76
|
-
text = all_lines.slice!(0, GRANULARITY)
|
77
|
-
|
78
|
-
# Try to get the next page
|
79
|
-
if text.nil? || text.count == 0
|
80
|
-
res = conn.exec("FETCH #{page_size} FROM #{table_name}")
|
81
|
-
if res.values.count == 0
|
82
|
-
chunks << chunk
|
83
|
-
break
|
84
|
-
end
|
85
|
-
all_lines = res.values.map { |r| r.join(',') }
|
86
|
-
next
|
87
|
-
end
|
88
|
-
|
89
|
-
# Work out the details for this chunk
|
90
|
-
line_count = text.count
|
91
|
-
text = text.join("\n")
|
92
|
-
text << "\n"
|
93
|
-
byte_size = text.bytesize
|
94
|
-
|
95
|
-
# Test whether limits would be exceeded
|
96
|
-
below_row_limit = chunk[:row_count] + line_count <= row_limit
|
97
|
-
below_size_limit = chunk[:length] + byte_size < CHUNK_LIMIT
|
98
|
-
|
99
|
-
if below_size_limit && below_row_limit
|
100
|
-
chunk[:length] += byte_size
|
101
|
-
chunk[:row_count] += line_count
|
102
|
-
else
|
103
|
-
chunks << chunk.dup
|
104
|
-
# Store the current pos since the next chunk isn't written
|
105
|
-
chunk[:start] = csvFile.pos
|
106
|
-
chunk[:length] = byte_size
|
107
|
-
chunk[:row_count] = line_count
|
108
|
-
end
|
109
|
-
|
110
|
-
csvFile.write(text)
|
111
|
-
end
|
112
|
-
end
|
113
|
-
|
114
|
-
conn.finish
|
115
|
-
|
116
|
-
# Close the file for this specific report
|
117
|
-
csvFile.close
|
118
|
-
@log.log_message "Report generated: #{report_name}"
|
119
|
-
end
|
120
|
-
|
121
|
-
chunks.each { |c| c.delete :row_count }
|
122
|
-
|
123
|
-
@log.log_message "Finished running query: #{query_name}"
|
124
|
-
chunks.to_json
|
125
|
-
end
|
126
|
-
|
127
|
-
def collection_ids
|
128
|
-
@log.log_message 'Retrieving list of site IDs'
|
129
|
-
connection = connect
|
130
|
-
query = 'select site_id from dim_site'
|
131
|
-
result = connection.query(query)
|
132
|
-
result.map { |r| r['site_id'] }
|
133
|
-
connection.finish
|
134
|
-
end
|
135
|
-
|
136
|
-
def get_cvss_version_strings(use_v3)
|
137
|
-
use_v3 ? { choice: '_v3', fallback: '' } : { choice: '', fallback: '' }
|
138
|
-
end
|
139
|
-
end
|
140
|
-
end
|