nexpose_servicenow 0.4.24 → 0.5.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +11 -3
- data/lib/nexpose_servicenow.rb +67 -58
- data/lib/nexpose_servicenow/arg_parser.rb +149 -51
- data/lib/nexpose_servicenow/chunker.rb +19 -14
- data/lib/nexpose_servicenow/historical_data.rb +179 -65
- data/lib/nexpose_servicenow/nexpose_helper.rb +75 -53
- data/lib/nexpose_servicenow/queries.rb +145 -34
- data/lib/nexpose_servicenow/version.rb +3 -3
- metadata +2 -2
@@ -1,3 +1,4 @@
|
|
1
|
+
#TODO: Check if using site_id is OK. If the object is changed, the code on SN will likely need changed.
|
1
2
|
|
2
3
|
module NexposeServiceNow
|
3
4
|
class Chunker
|
@@ -12,28 +13,28 @@ module NexposeServiceNow
|
|
12
13
|
|
13
14
|
def setup_logging
|
14
15
|
@log = NexposeServiceNow::NxLogger.instance
|
15
|
-
@log.log_message("
|
16
|
-
@log.log_message("Chunk Row Limit
|
16
|
+
@log.log_message("Chunk File Limit:\t#{@size_limit}MB")
|
17
|
+
@log.log_message("Chunk Row Limit:\t#{@row_limit}")
|
17
18
|
end
|
18
19
|
|
19
|
-
#Grab the header from the first file
|
20
|
+
# Grab the header from the first file
|
20
21
|
def get_header
|
21
|
-
file = File.open(@report_details.first[:report_name],
|
22
|
+
file = File.open(@report_details.first[:report_name], 'r')
|
22
23
|
header = file.readline
|
23
24
|
file.close
|
24
25
|
|
25
26
|
header
|
26
27
|
end
|
27
28
|
|
28
|
-
def preprocess
|
29
|
+
def preprocess
|
29
30
|
all_chunks = []
|
30
31
|
@report_details.each do |report|
|
31
|
-
@log.log_message("
|
32
|
+
@log.log_message("Dividing file #{report[:report_name]} into chunks.")
|
32
33
|
chunks = process_file(report[:report_name], report[:id])
|
33
34
|
all_chunks.concat chunks
|
34
35
|
end
|
35
36
|
|
36
|
-
@log.log_message("Files
|
37
|
+
@log.log_message("Files divided into #{all_chunks.count} chunks")
|
37
38
|
|
38
39
|
puts all_chunks.to_json
|
39
40
|
end
|
@@ -41,12 +42,12 @@ module NexposeServiceNow
|
|
41
42
|
def process_file(file_path, site_id=nil)
|
42
43
|
relative_size_limit = @size_limit - @header.bytesize
|
43
44
|
chunk = { site_id: site_id,
|
44
|
-
start: @header.bytesize,
|
45
|
+
start: @header.bytesize,
|
45
46
|
length: 0,
|
46
47
|
row_count: 0 }
|
47
48
|
|
48
49
|
chunks = []
|
49
|
-
csv_file = CSV.open(file_path,
|
50
|
+
csv_file = CSV.open(file_path, 'r', headers: true)
|
50
51
|
while(true)
|
51
52
|
position = csv_file.pos
|
52
53
|
line = csv_file.shift
|
@@ -62,12 +63,13 @@ module NexposeServiceNow
|
|
62
63
|
else
|
63
64
|
chunks << chunk
|
64
65
|
|
66
|
+
# TODO: Make generic?
|
65
67
|
#Initialise chunk with this row information
|
66
68
|
chunk = { site_id: site_id,
|
67
69
|
start: position,
|
68
70
|
length: row_length,
|
69
71
|
row_count: 1 }
|
70
|
-
end
|
72
|
+
end
|
71
73
|
end
|
72
74
|
csv_file.close
|
73
75
|
|
@@ -83,7 +85,7 @@ module NexposeServiceNow
|
|
83
85
|
end
|
84
86
|
|
85
87
|
def get_file(site_id=nil)
|
86
|
-
|
88
|
+
# -1 indicates a single query report
|
87
89
|
return @report_details.first[:report_name] if site_id.to_i <= 0
|
88
90
|
|
89
91
|
report = @report_details.find { |r| r[:id].to_s == site_id.to_s }
|
@@ -91,12 +93,15 @@ module NexposeServiceNow
|
|
91
93
|
end
|
92
94
|
|
93
95
|
def read_chunk(start, length, site_id=nil)
|
94
|
-
|
96
|
+
file_path = get_file(site_id)
|
97
|
+
msg = "Returning chunk. Start: #{start}, " \
|
98
|
+
"Length: #{length}, File: #{file_path}"
|
99
|
+
@log.log_message(msg)
|
95
100
|
|
96
101
|
#If the header isn't in the chunk, prepend it
|
97
|
-
header = start == 0 ?
|
102
|
+
header = start == 0 ? '' : @header
|
98
103
|
|
99
|
-
file = File.open(
|
104
|
+
file = File.open(file_path, 'rb')
|
100
105
|
file.seek(start)
|
101
106
|
puts header + file.read(length)
|
102
107
|
file.close
|
@@ -3,37 +3,81 @@ require_relative './nx_logger'
|
|
3
3
|
|
4
4
|
module NexposeServiceNow
|
5
5
|
class HistoricalData
|
6
|
-
REPORT_FILE = "Nexpose-ServiceNow-latest_scans.csv"
|
7
|
-
STORED_FILE = "last_scan_data.csv"
|
8
|
-
TIMESTAMP_FILE = "last_vuln_run.csv"
|
9
|
-
NEW_TIMESTAMP_FILE = "new_vuln_timestamp.csv"
|
10
6
|
|
11
|
-
|
12
|
-
|
13
|
-
|
7
|
+
REPORT_FILE = 'Nexpose-ServiceNow-latest_scans.csv'
|
8
|
+
STORED_FILE = 'last_scan_data.csv'
|
9
|
+
TIMESTAMP_FILE = 'last_vuln_run.csv'
|
10
|
+
NEW_TIMESTAMP_FILE = 'new_vuln_timestamp.csv'
|
11
|
+
|
12
|
+
DAG_TIMESTAMP_FILE = 'last_scan_data_dag.csv'
|
13
|
+
NEW_DAG_TIMESTAMP_FILE = 'new_dag_timestamp.csv'
|
14
|
+
|
15
|
+
SITE_IDENTIFIER = 'site_id'
|
16
|
+
SITE_DELTA_VALUE = 'last_scan_id'
|
17
|
+
SITE_BASE_VALUE = 0
|
18
|
+
|
19
|
+
DAG_IDENTIFIER = 'asset_group_id'
|
20
|
+
DAG_DELTA_VALUE = 'last_import'
|
21
|
+
DAG_BASE_VALUE = '1985-01-01 00:00:00'
|
22
|
+
|
23
|
+
def initialize(output_dir, nexpose_ids, id_type, start_time)
|
24
|
+
local_dir = File.expand_path(output_dir)
|
25
|
+
@ids = nexpose_ids
|
26
|
+
@id_type = id_type
|
27
|
+
|
28
|
+
if @id_type == :site
|
29
|
+
current_file = STORED_FILE
|
30
|
+
new_file = REPORT_FILE
|
31
|
+
elsif @id_type == :asset_group
|
32
|
+
current_file = DAG_TIMESTAMP_FILE
|
33
|
+
new_file = NEW_DAG_TIMESTAMP_FILE
|
34
|
+
end
|
35
|
+
|
36
|
+
@start_time = start_time
|
14
37
|
|
15
|
-
@local_file = File.join(local_dir,
|
16
|
-
@remote_file = File.join(local_dir,
|
38
|
+
@local_file = File.join(local_dir, current_file)
|
39
|
+
@remote_file = File.join(local_dir, new_file)
|
17
40
|
|
18
41
|
# File containing the timestamp used in vulnerability queries
|
19
42
|
@timestamp_file = File.join(local_dir, TIMESTAMP_FILE)
|
20
43
|
@prev_timestamp_file = File.join(local_dir, NEW_TIMESTAMP_FILE)
|
21
44
|
|
22
45
|
@log = NexposeServiceNow::NxLogger.instance
|
23
|
-
@log.log_message "Retrieving environment variables."
|
24
46
|
end
|
25
47
|
|
26
|
-
#
|
27
|
-
#
|
48
|
+
# TODO: Remove site references here? Will there be remote CSV here if we're using DAGs? No scan IDs.
|
49
|
+
# Filters the saved report down to the sites being queried
|
50
|
+
# This can then be used as a basis to update last_scan_data
|
28
51
|
def filter_report
|
29
|
-
#Create a full last_scan_data if it doesn't already exist
|
30
|
-
|
52
|
+
# Create a full last_scan_data if it doesn't already exist
|
53
|
+
create_base_delta_file unless File.exist? @local_file
|
31
54
|
|
32
|
-
@log.log_message 'Filtering report down sites which will be queried'
|
55
|
+
@log.log_message 'Filtering report down to sites which will be queried'
|
33
56
|
|
34
57
|
remote_csv = load_scan_id_report
|
35
|
-
|
36
|
-
|
58
|
+
nexpose_ids = @ids.map(&:to_s)
|
59
|
+
identifier = if @id_type == :site
|
60
|
+
SITE_IDENTIFIER
|
61
|
+
elsif @id_type ==:asset_group
|
62
|
+
DAG_IDENTIFIER
|
63
|
+
end
|
64
|
+
|
65
|
+
if @id_type == :asset_group
|
66
|
+
header = [DAG_IDENTIFIER, DAG_DELTA_VALUE]
|
67
|
+
rows = []
|
68
|
+
|
69
|
+
@ids.each do |i|
|
70
|
+
rows << CSV::Row.new(header, [i, @start_time])
|
71
|
+
end
|
72
|
+
|
73
|
+
remote_csv = CSV::Table.new(rows)
|
74
|
+
end
|
75
|
+
|
76
|
+
# TODO: Why is this done? Aren't these already filtered?
|
77
|
+
filtered_csv = remote_csv.delete_if do |r|
|
78
|
+
!nexpose_ids.include?(r[identifier])
|
79
|
+
end
|
80
|
+
|
37
81
|
File.open(@remote_file, 'w') do |f|
|
38
82
|
f.write(remote_csv.to_csv)
|
39
83
|
end
|
@@ -41,45 +85,58 @@ module NexposeServiceNow
|
|
41
85
|
puts filtered_csv
|
42
86
|
end
|
43
87
|
|
44
|
-
#Reads the downloaded report containing LATEST scan IDs
|
88
|
+
# Reads the downloaded report containing LATEST scan IDs
|
45
89
|
def load_scan_id_report
|
46
|
-
@log.log_message
|
90
|
+
@log.log_message 'Loading scan data report'
|
47
91
|
unless File.exists? @remote_file
|
48
|
-
@log.log_message
|
92
|
+
@log.log_message 'No existing report file found.'
|
49
93
|
return nil
|
50
94
|
end
|
51
95
|
CSV.read(@remote_file, headers: true)
|
52
96
|
end
|
53
97
|
|
54
|
-
#Loads the last scan data file as CSV.
|
55
|
-
#It may be necessary to create one first.
|
98
|
+
# Loads the last scan data file as CSV.
|
99
|
+
# It may be necessary to create one first.
|
56
100
|
def load_last_scan_data
|
57
|
-
@log.log_message
|
101
|
+
@log.log_message 'Loading last scan data.'
|
58
102
|
|
59
|
-
|
103
|
+
create_base_delta_file unless File.exist? @local_file
|
60
104
|
CSV.read(@local_file, headers: true)
|
61
105
|
end
|
62
106
|
|
63
|
-
def
|
107
|
+
def stored_delta_values(nexpose_ids)
|
64
108
|
return [] if !File.exist? @local_file
|
65
109
|
|
110
|
+
if @id_type == :site
|
111
|
+
identifier = SITE_IDENTIFIER
|
112
|
+
delta_column = SITE_DELTA_VALUE
|
113
|
+
base_value = SITE_BASE_VALUE
|
114
|
+
elsif @id_type == :asset_group
|
115
|
+
identifier = DAG_IDENTIFIER
|
116
|
+
delta_column = DAG_DELTA_VALUE
|
117
|
+
base_value = DAG_BASE_VALUE
|
118
|
+
end
|
119
|
+
|
66
120
|
csv = load_last_scan_data
|
67
|
-
|
68
|
-
|
69
|
-
row = csv.find { |r| r[
|
70
|
-
|
121
|
+
delta_values = {}
|
122
|
+
nexpose_ids.each do |id|
|
123
|
+
row = csv.find { |r| r[identifier] == id.to_s }
|
124
|
+
row ||= { delta_column => base_value }
|
125
|
+
delta_values[id.to_s] = row[delta_column]
|
71
126
|
end
|
72
127
|
|
73
|
-
|
128
|
+
delta_values
|
74
129
|
end
|
75
130
|
|
76
|
-
#Compares stored scan IDs versus remote scan IDs.
|
77
|
-
#This determines which scans are included as filters.
|
78
|
-
def
|
131
|
+
# Compares stored scan IDs versus remote scan IDs.
|
132
|
+
# This determines which scans are included as filters.
|
133
|
+
def collections_to_import(previously_imported_only=false)
|
79
134
|
return @ids unless File.exist? @remote_file
|
135
|
+
@log.log_message "Filtering for #{@id_type}s with new scans"
|
136
|
+
self.send("#{@id_type}s_to_import", previously_imported_only)
|
137
|
+
end
|
80
138
|
|
81
|
-
|
82
|
-
|
139
|
+
def sites_to_import(previously_imported_only=false)
|
83
140
|
remote_csv = CSV.read(@remote_file, headers: true)
|
84
141
|
local_csv = load_last_scan_data
|
85
142
|
|
@@ -93,7 +150,7 @@ module NexposeServiceNow
|
|
93
150
|
local_scan_id = local_scan_id['last_scan_id'] || 0
|
94
151
|
|
95
152
|
# Check if only allowing sites which were previously imported
|
96
|
-
next if local_scan_id.to_s ==
|
153
|
+
next if local_scan_id.to_s == '0' && previously_imported_only
|
97
154
|
|
98
155
|
filtered_sites << id if local_scan_id.to_i < remote_scan_id.to_i
|
99
156
|
end
|
@@ -101,22 +158,50 @@ module NexposeServiceNow
|
|
101
158
|
@ids = filtered_sites
|
102
159
|
end
|
103
160
|
|
104
|
-
|
105
|
-
|
106
|
-
|
161
|
+
def asset_groups_to_import(previously_imported_only=false)
|
162
|
+
filtered_asset_groups = []
|
163
|
+
local_csv = load_last_scan_data
|
164
|
+
|
165
|
+
@ids.each do |id|
|
166
|
+
local_id = local_csv.find { |r| r[DAG_IDENTIFIER] == id.to_s } || {}
|
167
|
+
local_id = local_id[DAG_DELTA_VALUE] || DAG_BASE_VALUE
|
168
|
+
|
169
|
+
next if local_id == DAG_BASE_VALUE && previously_imported_only
|
170
|
+
|
171
|
+
filtered_asset_groups << id
|
172
|
+
end
|
107
173
|
|
174
|
+
@ids = filtered_asset_groups
|
175
|
+
end
|
176
|
+
|
177
|
+
# Creates a base last scan data file from a downloaded report
|
178
|
+
def create_base_delta_file
|
179
|
+
@log.log_message 'Creating base delta file'
|
180
|
+
self.send("create_#{@id_type}_base_file")
|
181
|
+
end
|
182
|
+
|
183
|
+
def create_site_base_file
|
108
184
|
csv = load_scan_id_report
|
109
185
|
csv.delete('finished')
|
110
|
-
csv.each { |l| l['last_scan_id'] =
|
186
|
+
csv.each { |l| l['last_scan_id'] = SITE_BASE_VALUE }
|
111
187
|
|
112
188
|
save_last_scan_data(csv)
|
113
189
|
end
|
114
190
|
|
115
|
-
|
116
|
-
|
117
|
-
|
191
|
+
def create_asset_group_base_file
|
192
|
+
CSV.open(@local_file, 'w') do |csv|
|
193
|
+
csv << %w(asset_group_id last_import)
|
194
|
+
@ids.each do |n|
|
195
|
+
csv << [n, DAG_BASE_VALUE]
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
# Updates only the rows that were affected by this scan
|
201
|
+
def update_delta_file
|
202
|
+
@log.log_message 'Updating last scan data'
|
118
203
|
|
119
|
-
if !(File.exist? @local_file) && !(File.exist? @remote_file)
|
204
|
+
if !(File.exist? @local_file) && !(File.exist? @remote_file)
|
120
205
|
@log.log_message 'Last scan data does not exist yet.'
|
121
206
|
return
|
122
207
|
end
|
@@ -124,23 +209,51 @@ module NexposeServiceNow
|
|
124
209
|
updated_csv = load_last_scan_data
|
125
210
|
remote_csv = load_scan_id_report
|
126
211
|
|
212
|
+
method = "update_#{@id_type}_delta_file"
|
213
|
+
updated_csv = self.send(method, updated_csv, remote_csv)
|
214
|
+
|
215
|
+
save_last_scan_data(updated_csv)
|
216
|
+
end
|
217
|
+
|
218
|
+
def update_site_delta_file(updated_csv, remote_csv)
|
127
219
|
#merge changes in from remote_csv
|
128
220
|
remote_csv.each do |row|
|
129
221
|
updated_row = updated_csv.find { |r| r['site_id'] == row['site_id'] }
|
130
222
|
if updated_row.nil?
|
131
223
|
row.delete 'finished'
|
132
|
-
updated_csv << row
|
224
|
+
updated_csv << row
|
133
225
|
else
|
134
|
-
updated_row['last_scan_id'] = row['last_scan_id']
|
226
|
+
updated_row['last_scan_id'] = row['last_scan_id']
|
135
227
|
end
|
136
228
|
end
|
137
229
|
|
138
|
-
|
230
|
+
updated_csv
|
231
|
+
end
|
232
|
+
|
233
|
+
def update_asset_group_delta_file(updated_csv, remote_csv)
|
234
|
+
#merge changes in from remote_csv
|
235
|
+
remote_csv.each do |row|
|
236
|
+
updated_row = updated_csv.find do |r|
|
237
|
+
r['asset_group_id'] == row['asset_group_id']
|
238
|
+
end
|
139
239
|
|
140
|
-
|
240
|
+
if updated_row.nil?
|
241
|
+
updated_csv << row
|
242
|
+
else
|
243
|
+
updated_row['last_import'] = row['last_import']
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
# Insert any NEW IDs with baseline time
|
248
|
+
@ids.each do |i|
|
249
|
+
row = updated_csv.find { |r| r[DAG_IDENTIFIER] == i }
|
250
|
+
updated_csv << [i, DAG_BASE_VALUE] if row.nil?
|
251
|
+
end
|
252
|
+
|
253
|
+
updated_csv
|
141
254
|
end
|
142
255
|
|
143
|
-
#Overwrite the last scan data file with new csv
|
256
|
+
# Overwrite the last scan data file with new csv
|
144
257
|
def save_last_scan_data(csv)
|
145
258
|
@log.log_message 'Saving last scan data'
|
146
259
|
File.open(@local_file, 'w') do |f|
|
@@ -148,9 +261,7 @@ module NexposeServiceNow
|
|
148
261
|
end
|
149
262
|
end
|
150
263
|
|
151
|
-
|
152
|
-
#insert sites?
|
153
|
-
def save_vuln_timestamp(sites=[])
|
264
|
+
def save_vuln_timestamp(nexpose_ids=[])
|
154
265
|
start_time = Time.new
|
155
266
|
|
156
267
|
#Read timestamp from new timestamp file (substitute base time)
|
@@ -167,32 +278,32 @@ module NexposeServiceNow
|
|
167
278
|
|
168
279
|
last_run ||= Time.new(1985)
|
169
280
|
last_sites ||= []
|
170
|
-
last_run = last_run.strftime(
|
281
|
+
last_run = last_run.strftime('%Y-%m-%d') if last_run.class.to_s == 'Time'
|
171
282
|
create_last_vuln_data(last_run, last_sites)
|
172
283
|
|
173
284
|
file = File.expand_path(@prev_timestamp_file)
|
174
285
|
CSV.open(file, 'w') do |csv|
|
175
286
|
csv << ['Last Scan Time', 'Sites']
|
176
|
-
csv << [start_time.strftime(
|
287
|
+
csv << [start_time.strftime('%Y-%m-%d'), nexpose_ids.join(',')]
|
177
288
|
end
|
178
289
|
end
|
179
290
|
|
180
|
-
def create_last_vuln_data(time=nil,
|
291
|
+
def create_last_vuln_data(time=nil, nexpose_ids=[])
|
181
292
|
@log.log_message 'Creating last vulnerability scan time file.'
|
182
293
|
|
183
294
|
time ||= Time.new(1985)
|
184
|
-
time = time.strftime(
|
185
|
-
|
295
|
+
time = time.strftime('%Y-%m-%d') if time.class.to_s == 'Time'
|
296
|
+
nexpose_ids = nexpose_ids.join(',') if nexpose_ids.class.to_s == 'Array'
|
186
297
|
|
187
298
|
file = File.expand_path(@timestamp_file)
|
188
299
|
|
189
300
|
CSV.open(file, 'w') do |csv|
|
190
301
|
csv << ['Last Scan Time', 'Sites']
|
191
|
-
csv << [time,
|
302
|
+
csv << [time, nexpose_ids]
|
192
303
|
end
|
193
304
|
end
|
194
305
|
|
195
|
-
#Current IDs are inserted into the updated CSV file.
|
306
|
+
# Current IDs are inserted into the updated CSV file.
|
196
307
|
def last_vuln_run
|
197
308
|
@log.log_message 'Retrieving the last vulnerability timestamp'
|
198
309
|
|
@@ -206,8 +317,11 @@ module NexposeServiceNow
|
|
206
317
|
last_run
|
207
318
|
end
|
208
319
|
|
209
|
-
|
210
|
-
#
|
320
|
+
#########################################################
|
321
|
+
# Experimental #
|
322
|
+
#########################################################
|
323
|
+
|
324
|
+
# These should probably return strings that can be mlog'd
|
211
325
|
def log_and_print(message)
|
212
326
|
puts message
|
213
327
|
@log.log_message message unless @log.nil?
|
@@ -227,15 +341,15 @@ module NexposeServiceNow
|
|
227
341
|
|
228
342
|
csv = load_last_scan_data
|
229
343
|
row = csv.find { |r| r['site_id'] == nexpose_id }
|
230
|
-
|
344
|
+
|
231
345
|
if row.nil?
|
232
346
|
csv << [nexpose_id, scan_id]
|
233
347
|
else
|
234
348
|
row['last_scan_id'] = scan_id
|
235
349
|
end
|
236
|
-
|
350
|
+
|
237
351
|
save_last_scan_data csv
|
238
|
-
|
352
|
+
|
239
353
|
log_and_print 'Last scan data updated.'
|
240
354
|
end
|
241
355
|
|
@@ -253,7 +367,7 @@ module NexposeServiceNow
|
|
253
367
|
|
254
368
|
new_name = "#{filename}.#{Time.new.strftime('%Y-%m-%d.%H:%M:%S')}"
|
255
369
|
begin
|
256
|
-
#Delete existing file with same name
|
370
|
+
# Delete existing file with same name
|
257
371
|
File.delete new_name if File.exist? new_name
|
258
372
|
File.rename(filename, new_name)
|
259
373
|
rescue Exception => e
|