nexpose 0.9.8 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/lib/nexpose.rb +8 -4
- data/lib/nexpose/ajax.rb +29 -4
- data/lib/nexpose/alert.rb +160 -177
- data/lib/nexpose/api.rb +18 -0
- data/lib/nexpose/common.rb +144 -10
- data/lib/nexpose/credential.rb +185 -1
- data/lib/nexpose/discovery.rb +141 -16
- data/lib/nexpose/discovery/filter.rb +26 -3
- data/lib/nexpose/engine.rb +16 -0
- data/lib/nexpose/json_serializer.rb +92 -0
- data/lib/nexpose/scan.rb +131 -23
- data/lib/nexpose/scan_template.rb +1 -1
- data/lib/nexpose/shared_secret.rb +31 -0
- data/lib/nexpose/site.rb +339 -317
- data/lib/nexpose/site_credentials.rb +178 -0
- data/lib/nexpose/tag.rb +42 -1
- data/lib/nexpose/util.rb +11 -16
- data/lib/nexpose/version.rb +1 -1
- data/lib/nexpose/wait.rb +103 -0
- data/lib/nexpose/web_credentials.rb +252 -0
- metadata +18 -8
- data/lib/nexpose/site_credential.rb +0 -323
@@ -4,7 +4,7 @@ module Nexpose
|
|
4
4
|
|
5
5
|
# List the scan templates currently configured on the console.
|
6
6
|
#
|
7
|
-
# @return [Array[
|
7
|
+
# @return [Array[ScanTemplateSummary]] list of scan template summary objects.
|
8
8
|
#
|
9
9
|
def list_scan_templates
|
10
10
|
templates = JSON.parse(AJAX.get(self, '/api/2.0/scan_templates'))
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Nexpose
|
2
|
+
# SharedSecret class for pairing engines
|
3
|
+
class SharedSecret < APIObject
|
4
|
+
attr_reader :key_string
|
5
|
+
attr_reader :ttl
|
6
|
+
|
7
|
+
def initialize(console, time_to_live)
|
8
|
+
uri = "/data/admin/global/shared-secret?time-to-live=#{time_to_live}"
|
9
|
+
json = JSON.parse(AJAX.put(console, uri))
|
10
|
+
self.from_json(json)
|
11
|
+
end
|
12
|
+
|
13
|
+
def from_json(json)
|
14
|
+
@key_string = json['keyString']
|
15
|
+
@ttl = json['timeToLiveInSeconds']
|
16
|
+
end
|
17
|
+
|
18
|
+
def delete(console)
|
19
|
+
uri = "/data/admin/global/remove-shared-secret?key-string=#{key_string}"
|
20
|
+
AJAX.delete(console, uri)
|
21
|
+
end
|
22
|
+
|
23
|
+
def ==(other)
|
24
|
+
return false unless self.class == other.class
|
25
|
+
return false unless key_string.upcase == other.key_string.upcase
|
26
|
+
|
27
|
+
true
|
28
|
+
end
|
29
|
+
alias_method :eql?, :==
|
30
|
+
end
|
31
|
+
end
|
data/lib/nexpose/site.rb
CHANGED
@@ -77,8 +77,8 @@ module Nexpose
|
|
77
77
|
# Configuration object representing a Nexpose site.
|
78
78
|
#
|
79
79
|
# For a basic walk-through, see {https://github.com/rapid7/nexpose-client/wiki/Using-Sites}
|
80
|
-
class Site
|
81
|
-
|
80
|
+
class Site < APIObject
|
81
|
+
include JsonSerializer
|
82
82
|
# The site ID. An ID of -1 is used to designate a site that has not been
|
83
83
|
# saved to a Nexpose console.
|
84
84
|
attr_accessor :id
|
@@ -89,33 +89,41 @@ module Nexpose
|
|
89
89
|
# Description of the site.
|
90
90
|
attr_accessor :description
|
91
91
|
|
92
|
-
#
|
93
|
-
|
94
|
-
# @see IPRange
|
95
|
-
attr_accessor :assets
|
92
|
+
# Included scan targets. May be IPv4, IPv6, DNS names, IPRanges or assetgroup ids.
|
93
|
+
attr_accessor :included_scan_targets
|
96
94
|
|
97
|
-
#
|
98
|
-
attr_accessor :
|
95
|
+
# Excluded scan targets. May be IPv4, IPv6, DNS names, IPRanges or assetgroup ids.
|
96
|
+
attr_accessor :excluded_scan_targets
|
99
97
|
|
100
|
-
# Scan template to use when starting a scan job. Default: full-audit
|
101
|
-
attr_accessor :
|
98
|
+
# Scan template to use when starting a scan job. Default: full-audit-without-web-spider
|
99
|
+
attr_accessor :scan_template_id
|
102
100
|
|
103
101
|
# Friendly name of scan template to use when starting a scan job.
|
104
102
|
# Value is populated when a site is saved or loaded from a console.
|
105
103
|
attr_accessor :scan_template_name
|
106
104
|
|
107
105
|
# Scan Engine to use. Will use the default engine if nil or -1.
|
108
|
-
attr_accessor :
|
106
|
+
attr_accessor :engine_id
|
109
107
|
|
110
108
|
# [Array] Schedule starting dates and times for scans, and set their frequency.
|
111
109
|
attr_accessor :schedules
|
112
110
|
|
111
|
+
|
113
112
|
# The risk factor associated with this site. Default: 1.0
|
114
113
|
attr_accessor :risk_factor
|
115
114
|
|
116
115
|
# [Array] Collection of credentials associated with this site. Does not
|
117
116
|
# include shared credentials.
|
118
|
-
attr_accessor :
|
117
|
+
attr_accessor :site_credentials
|
118
|
+
|
119
|
+
# [Array] Collection of shared credentials associated with this site.
|
120
|
+
attr_accessor :shared_credentials
|
121
|
+
|
122
|
+
# [Array] Collection of web credentials associated with the site.
|
123
|
+
attr_accessor :web_credentials
|
124
|
+
|
125
|
+
# Scan the assets with last scanned engine or not.
|
126
|
+
attr_accessor :auto_engine_selection_enabled
|
119
127
|
|
120
128
|
# [Array] Collection of real-time alerts.
|
121
129
|
# @see Alert
|
@@ -134,15 +142,11 @@ module Nexpose
|
|
134
142
|
# Configuration version. Default: 3
|
135
143
|
attr_accessor :config_version
|
136
144
|
|
137
|
-
# Whether or not this site is dynamic.
|
138
|
-
# Dynamic sites are created through Asset Discovery Connections.
|
139
|
-
attr_accessor :is_dynamic
|
140
|
-
|
141
145
|
# Asset filter criteria if this site is dynamic.
|
142
|
-
attr_accessor :
|
146
|
+
attr_accessor :search_criteria
|
143
147
|
|
144
|
-
#
|
145
|
-
attr_accessor :
|
148
|
+
# discovery config of the discovery connection associated with this site if it is dynamic.
|
149
|
+
attr_accessor :discovery_config
|
146
150
|
|
147
151
|
# [Array[TagSummary]] Collection of TagSummary
|
148
152
|
attr_accessor :tags
|
@@ -150,166 +154,380 @@ module Nexpose
|
|
150
154
|
# Site constructor. Both arguments are optional.
|
151
155
|
#
|
152
156
|
# @param [String] name Unique name of the site.
|
153
|
-
# @param [String]
|
154
|
-
def initialize(name = nil,
|
157
|
+
# @param [String] scan_template_id ID of the scan template to use.
|
158
|
+
def initialize(name = nil, scan_template_id = 'full-audit-without-web-spider')
|
155
159
|
@name = name
|
156
|
-
@
|
157
|
-
|
160
|
+
@scan_template_id = scan_template_id
|
158
161
|
@id = -1
|
159
162
|
@risk_factor = 1.0
|
160
163
|
@config_version = 3
|
161
|
-
@is_dynamic = false
|
162
|
-
@assets = []
|
163
164
|
@schedules = []
|
164
|
-
@
|
165
|
+
@included_scan_targets = { addresses: [], asset_groups: [] }
|
166
|
+
@excluded_scan_targets = { addresses: [], asset_groups: [] }
|
167
|
+
@site_credentials = []
|
168
|
+
@shared_credentials = []
|
169
|
+
@web_credentials = []
|
165
170
|
@alerts = []
|
166
|
-
@exclude = []
|
167
171
|
@users = []
|
168
172
|
@tags = []
|
169
173
|
end
|
170
174
|
|
171
|
-
# Returns
|
172
|
-
|
173
|
-
|
175
|
+
# Returns the array of included scan target addresses.
|
176
|
+
# @return [Array[IPRange|HostName]] Array of included addresses.
|
177
|
+
def included_addresses
|
178
|
+
@included_scan_targets[:addresses]
|
174
179
|
end
|
175
180
|
|
176
|
-
|
177
|
-
|
178
|
-
|
181
|
+
# Sets the array of included scan target addresses.
|
182
|
+
# @param [Array[IPRange|HostName]] new_addresses The new array of scan target addresses.
|
183
|
+
# @return [Array[IPRange|HostName]] Array of updated scan target addresses.
|
184
|
+
def included_addresses=(new_addresses)
|
185
|
+
@included_scan_targets[:addresses] = new_addresses
|
179
186
|
end
|
180
187
|
|
181
|
-
#
|
182
|
-
#
|
183
|
-
|
184
|
-
|
185
|
-
@assets << HostName.new(hostname)
|
188
|
+
# Returns the array of IDs for included scan target asset groups.
|
189
|
+
# @return [Array[Fixnum]] Array of included asset groups.
|
190
|
+
def included_asset_groups
|
191
|
+
@included_scan_targets[:asset_groups]
|
186
192
|
end
|
187
193
|
|
188
|
-
#
|
189
|
-
#
|
190
|
-
# @
|
191
|
-
def
|
192
|
-
@
|
194
|
+
# Sets the array of IDs for included scan target asset groups.
|
195
|
+
# @param [Array[Fixnum] new_asset_groups The new array of IDs for scan target asset groups.
|
196
|
+
# @return [Array[Fixnum] Array of IDs of the updated scan target asset groups.
|
197
|
+
def included_asset_groups=(new_asset_groups)
|
198
|
+
@included_scan_targets[:asset_groups] = new_asset_groups
|
193
199
|
end
|
194
200
|
|
195
|
-
#
|
196
|
-
#
|
197
|
-
|
198
|
-
|
199
|
-
@assets << IPRange.new(ip)
|
201
|
+
# Returns the array of excluded scan target addresses.
|
202
|
+
# @return [Array[IPRange|HostName]] Array of excluded addresses.
|
203
|
+
def excluded_addresses
|
204
|
+
@excluded_scan_targets[:addresses]
|
200
205
|
end
|
201
206
|
|
202
|
-
#
|
203
|
-
#
|
204
|
-
# @
|
205
|
-
def
|
206
|
-
@
|
207
|
+
# Sets the array of excluded scan target addresses.
|
208
|
+
# @param [Array[IPRange|HostName]] new_addresses The new array of scan target addresses.
|
209
|
+
# @return [Array[IPRange|HostName]] Array of updated scan target addresses.
|
210
|
+
def excluded_addresses=(new_addresses)
|
211
|
+
@excluded_scan_targets[:addresses] = new_addresses
|
212
|
+
end
|
213
|
+
|
214
|
+
# Returns the array of IDs for excluded scan target asset groups.
|
215
|
+
# @return [Array[Fixnum]] Array of IDs for excluded asset groups.
|
216
|
+
def excluded_asset_groups
|
217
|
+
@excluded_scan_targets[:asset_groups]
|
218
|
+
end
|
219
|
+
|
220
|
+
# Sets the array IDs for excluded scan target asset groups.
|
221
|
+
# @param [Array[Fixnum]] new_asset_groups The new array of IDs for scan target asset groups.
|
222
|
+
# @return [Array[Fixnum]] Array of IDs of the updated scan target asset groups.
|
223
|
+
def excluded_asset_groups=(new_asset_groups)
|
224
|
+
@excluded_scan_targets[:asset_groups] = new_asset_groups
|
225
|
+
end
|
226
|
+
|
227
|
+
# Returns true when the site is dynamic.
|
228
|
+
def is_dynamic?
|
229
|
+
!@discovery_config.nil?
|
207
230
|
end
|
208
231
|
|
209
232
|
# Adds assets to this site by IP address range.
|
210
233
|
#
|
211
234
|
# @param [String] from Beginning IP address of a range.
|
212
235
|
# @param [String] to Ending IP address of a range.
|
236
|
+
def include_ip_range(from, to)
|
237
|
+
begin
|
238
|
+
from_ip = IPAddr.new(from)
|
239
|
+
to_ip = IPAddr.new(to)
|
240
|
+
(from_ip..to_ip)
|
241
|
+
if (from_ip..to_ip).to_a.size == 0
|
242
|
+
raise 'Invalid IP range specified'
|
243
|
+
end
|
244
|
+
@included_scan_targets[:addresses] << IPRange.new(from, to)
|
245
|
+
rescue ArgumentError => e
|
246
|
+
raise "#{e.message} in given IP range"
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
# @deprecated Use {#include_ip_range} instead.
|
213
251
|
def add_ip_range(from, to)
|
214
|
-
|
252
|
+
warn "[DEPRECATED] Use #{self.class}#include_ip_range instead of #{self.class}#add_ip_range."
|
253
|
+
include_ip_range(from, to)
|
215
254
|
end
|
216
255
|
|
217
256
|
# Remove assets to this site by IP address range.
|
218
257
|
#
|
219
258
|
# @param [String] from Beginning IP address of a range.
|
220
259
|
# @param [String] to Ending IP address of a range.
|
260
|
+
def remove_included_ip_range(from, to)
|
261
|
+
begin
|
262
|
+
from_ip = IPAddr.new(from)
|
263
|
+
to_ip = IPAddr.new(to)
|
264
|
+
(from_ip..to_ip)
|
265
|
+
if (from_ip..to_ip).to_a.size == 0
|
266
|
+
raise 'Invalid IP range specified'
|
267
|
+
end
|
268
|
+
@included_scan_targets[:addresses].reject! { |t| t.eql? IPRange.new(from, to) }
|
269
|
+
rescue ArgumentError => e
|
270
|
+
raise "#{e.message} in given IP range"
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
# @deprecated Use {#remove_included_ip_range} instead.
|
221
275
|
def remove_ip_range(from, to)
|
222
|
-
|
276
|
+
warn "[DEPRECATED] Use #{self.class}#remove_included_ip_range instead of #{self.class}#remove_ip_range."
|
277
|
+
remove_included_ip_range(from, to)
|
223
278
|
end
|
224
279
|
|
225
|
-
# Adds an asset to this site, resolving whether an IP or hostname is
|
280
|
+
# Adds an asset to this site included scan targets, resolving whether an IP or hostname is
|
226
281
|
# provided.
|
227
282
|
#
|
228
283
|
# @param [String] asset Identifier of an asset, either IP or host name.
|
229
284
|
#
|
285
|
+
def include_asset(asset)
|
286
|
+
@included_scan_targets[:addresses] << HostOrIP.convert(asset)
|
287
|
+
end
|
288
|
+
|
289
|
+
# @deprecated Use {#include_asset} instead.
|
230
290
|
def add_asset(asset)
|
231
|
-
|
232
|
-
|
291
|
+
warn "[DEPRECATED] Use #{self.class}#include_asset instead of #{self.class}#add_asset."
|
292
|
+
include_asset(asset)
|
233
293
|
end
|
234
294
|
|
235
|
-
|
295
|
+
alias_method :add_host, :add_asset
|
296
|
+
alias_method :add_ip, :add_asset
|
297
|
+
|
298
|
+
# Remove an asset to this site included scan targets, resolving whether an IP or hostname is
|
236
299
|
# provided.
|
237
300
|
#
|
238
301
|
# @param [String] asset Identifier of an asset, either IP or host name.
|
239
302
|
#
|
303
|
+
def remove_included_asset(asset)
|
304
|
+
@included_scan_targets[:addresses].reject! { |existing_asset| existing_asset == HostOrIP.convert(asset) }
|
305
|
+
end
|
306
|
+
|
307
|
+
# @deprecated Use {#remove_included_asset} instead.
|
240
308
|
def remove_asset(asset)
|
309
|
+
warn "[DEPRECATED] Use #{self.class}#remove_included_asset instead of #{self.class}#remove_asset."
|
310
|
+
remove_included_asset(asset)
|
311
|
+
end
|
312
|
+
|
313
|
+
alias_method :remove_host, :remove_asset
|
314
|
+
alias_method :remove_ip, :remove_asset
|
315
|
+
|
316
|
+
# Adds assets to this site excluded scan targets by IP address range.
|
317
|
+
#
|
318
|
+
# @param [String] from Beginning IP address of a range.
|
319
|
+
# @param [String] to Ending IP address of a range.
|
320
|
+
def exclude_ip_range(from, to)
|
241
321
|
begin
|
242
|
-
|
243
|
-
IPAddr.new(
|
244
|
-
|
322
|
+
from_ip = IPAddr.new(from)
|
323
|
+
to_ip = IPAddr.new(to)
|
324
|
+
(from_ip..to_ip)
|
325
|
+
if (from_ip..to_ip).to_a.size == 0
|
326
|
+
raise 'Invalid IP range specified'
|
327
|
+
end
|
328
|
+
@excluded_scan_targets[:addresses] << IPRange.new(from, to)
|
245
329
|
rescue ArgumentError => e
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
330
|
+
raise "#{e.message} in given IP range"
|
331
|
+
end
|
332
|
+
end
|
333
|
+
|
334
|
+
# Remove assets from this site excluded scan targets by IP address range.
|
335
|
+
#
|
336
|
+
# @param [String] from Beginning IP address of a range.
|
337
|
+
# @param [String] to Ending IP address of a range.
|
338
|
+
def remove_excluded_ip_range(from, to)
|
339
|
+
begin
|
340
|
+
from_ip = IPAddr.new(from)
|
341
|
+
to_ip = IPAddr.new(to)
|
342
|
+
(from_ip..to_ip)
|
343
|
+
if (from_ip..to_ip).to_a.size == 0
|
344
|
+
raise 'Invalid IP range specified'
|
250
345
|
end
|
346
|
+
@excluded_scan_targets[:addresses].reject! { |t| t.eql? IPRange.new(from, to) }
|
347
|
+
rescue ArgumentError => e
|
348
|
+
raise "#{e.message} in given IP range"
|
251
349
|
end
|
252
350
|
end
|
253
351
|
|
254
|
-
# Adds an asset to this site
|
255
|
-
#
|
352
|
+
# Adds an asset to this site excluded scan targets, resolving whether an IP or hostname is
|
353
|
+
# provided.
|
256
354
|
#
|
257
355
|
# @param [String] asset Identifier of an asset, either IP or host name.
|
258
356
|
#
|
259
357
|
def exclude_asset(asset)
|
260
|
-
@
|
358
|
+
@excluded_scan_targets[:addresses] << HostOrIP.convert(asset)
|
261
359
|
end
|
262
360
|
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
# Remove an asset from this site's exclude list, resolving whether an IP
|
267
|
-
# or hostname is provided.
|
361
|
+
# Removes an asset to this site excluded scan targets, resolving whether an IP or hostname is
|
362
|
+
# provided.
|
268
363
|
#
|
269
364
|
# @param [String] asset Identifier of an asset, either IP or host name.
|
270
365
|
#
|
271
366
|
def remove_excluded_asset(asset)
|
272
|
-
@
|
367
|
+
@excluded_scan_targets[:addresses].reject! { |existing_asset| existing_asset == HostOrIP.convert(asset) }
|
273
368
|
end
|
274
369
|
|
275
|
-
|
276
|
-
|
370
|
+
# Adds an asset group ID to this site included scan targets.
|
371
|
+
#
|
372
|
+
# @param [Integer] asset_group_id Identifier of an assetGroupID.
|
373
|
+
#
|
374
|
+
def include_asset_group(asset_group_id)
|
375
|
+
validate_asset_group(asset_group_id)
|
376
|
+
@included_scan_targets[:asset_groups] << asset_group_id.to_i
|
377
|
+
end
|
277
378
|
|
278
|
-
# Adds
|
379
|
+
# Adds an asset group ID to this site included scan targets.
|
279
380
|
#
|
280
|
-
# @param [
|
281
|
-
#
|
282
|
-
def
|
283
|
-
|
381
|
+
# @param [Integer] asset_group_id Identifier of an assetGroupID.
|
382
|
+
#
|
383
|
+
def remove_included_asset_group(asset_group_id)
|
384
|
+
validate_asset_group(asset_group_id)
|
385
|
+
@included_scan_targets[:asset_groups].reject! { |t| t.eql? asset_group_id.to_i }
|
284
386
|
end
|
285
387
|
|
286
|
-
#
|
388
|
+
# Adds an asset group ID to this site excluded scan targets.
|
287
389
|
#
|
288
|
-
# @param [
|
289
|
-
#
|
290
|
-
def
|
291
|
-
|
390
|
+
# @param [Integer] asset_group_id Identifier of an assetGroupID.
|
391
|
+
#
|
392
|
+
def exclude_asset_group(asset_group_id)
|
393
|
+
validate_asset_group(asset_group_id)
|
394
|
+
@excluded_scan_targets[:asset_groups] << asset_group_id.to_i
|
292
395
|
end
|
293
396
|
|
294
|
-
#
|
397
|
+
# Adds an asset group ID to this site excluded scan targets.
|
295
398
|
#
|
296
|
-
# @param [
|
297
|
-
# @param [Fixnum] id Site ID of an existing site.
|
298
|
-
# @return [Site] Site configuration loaded from a Nexpose console.
|
399
|
+
# @param [Integer] asset_group_id Identifier of an assetGroupID.
|
299
400
|
#
|
300
|
-
def
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
401
|
+
def remove_excluded_asset_group(asset_group_id)
|
402
|
+
validate_asset_group(asset_group_id)
|
403
|
+
@excluded_scan_targets[:asset_groups].reject! { |t| t.eql? asset_group_id.to_i }
|
404
|
+
end
|
405
|
+
|
406
|
+
def validate_asset_group(asset_group_id)
|
407
|
+
begin
|
408
|
+
Integer(asset_group_id)
|
409
|
+
rescue ArgumentError => e
|
410
|
+
raise "Invalid asset_group id. #{e.message}"
|
411
|
+
end
|
412
|
+
|
413
|
+
raise 'Invalid asset_group id. Must be positive number.' if asset_group_id.to_i < 1
|
414
|
+
end
|
415
|
+
|
416
|
+
def add_user(user_id)
|
417
|
+
unless user_id.is_a?(Numeric) && user_id > 0
|
418
|
+
raise 'Invalid user id. A user id must be a positive number and refer to an existing system user.'
|
419
|
+
end
|
420
|
+
|
421
|
+
@users << { id: user_id}
|
422
|
+
end
|
423
|
+
|
424
|
+
def remove_user(user_id)
|
425
|
+
unless user_id.is_a?(Numeric) && user_id > 0
|
426
|
+
raise 'Invalid user id. A user id must be a positive number and refer to an existing system user.'
|
427
|
+
end
|
428
|
+
|
429
|
+
@users.delete_if { |h| h[:id] == user_id }
|
430
|
+
end
|
431
|
+
|
432
|
+
def self.from_hash(hash)
|
433
|
+
site = new(hash[:name], hash[:scan_template_id])
|
434
|
+
hash.each do |k, v|
|
435
|
+
site.instance_variable_set("@#{k}", v)
|
307
436
|
end
|
308
|
-
|
309
|
-
|
437
|
+
|
438
|
+
# Convert each string address to either a HostName or IPRange object
|
439
|
+
included_scan_targets = { addresses: [], asset_groups: [] }
|
440
|
+
site.included_scan_targets[:addresses].each { |asset| included_scan_targets[:addresses] << HostOrIP.convert(asset) }
|
441
|
+
included_scan_targets[:asset_groups] = site.included_scan_targets[:asset_groups]
|
442
|
+
site.included_scan_targets = included_scan_targets
|
443
|
+
|
444
|
+
excluded_scan_targets = { addresses: [], asset_groups: [] }
|
445
|
+
site.excluded_scan_targets[:addresses].each { |asset| excluded_scan_targets[:addresses] << HostOrIP.convert(asset) }
|
446
|
+
excluded_scan_targets[:asset_groups] = site.excluded_scan_targets[:asset_groups]
|
447
|
+
site.excluded_scan_targets = excluded_scan_targets
|
448
|
+
|
310
449
|
site
|
311
450
|
end
|
312
451
|
|
452
|
+
def to_json
|
453
|
+
JSON.generate(to_h)
|
454
|
+
end
|
455
|
+
|
456
|
+
def to_h
|
457
|
+
included_scan_targets = {
|
458
|
+
addresses: @included_scan_targets[:addresses].compact,
|
459
|
+
asset_groups: @included_scan_targets[:asset_groups].compact
|
460
|
+
}
|
461
|
+
excluded_scan_targets = {
|
462
|
+
addresses: @excluded_scan_targets[:addresses].compact,
|
463
|
+
asset_groups: @excluded_scan_targets[:asset_groups].compact
|
464
|
+
}
|
465
|
+
|
466
|
+
{
|
467
|
+
id: @id,
|
468
|
+
name: @name,
|
469
|
+
description: @description,
|
470
|
+
auto_engine_selection_enabled: @auto_engine_selection_enabled,
|
471
|
+
included_scan_targets: included_scan_targets,
|
472
|
+
excluded_scan_targets: excluded_scan_targets,
|
473
|
+
engine_id: @engine_id,
|
474
|
+
scan_template_id: @scan_template_id,
|
475
|
+
risk_factor: @risk_factor,
|
476
|
+
schedules: (@schedules || []).map {|schedule| schedule.to_h},
|
477
|
+
shared_credentials: (@shared_credentials || []).map {|cred| cred.to_h},
|
478
|
+
site_credentials: (@site_credentials || []).map {|cred| cred.to_h},
|
479
|
+
web_credentials: (@web_credentials || []).map {|webCred| webCred.to_h},
|
480
|
+
discovery_config: @discovery_config.to_h,
|
481
|
+
search_criteria: @search_criteria.to_h,
|
482
|
+
tags: (@tags || []).map{|tag| tag.to_h},
|
483
|
+
alerts: (@alerts || []).map {|alert| alert.to_h },
|
484
|
+
organization: @organization.to_h,
|
485
|
+
users: users
|
486
|
+
}
|
487
|
+
end
|
488
|
+
|
489
|
+
require 'json'
|
490
|
+
# Load an site from the provided console.
|
491
|
+
#
|
492
|
+
# @param [Connection] nsc Active connection to a Nexpose console.
|
493
|
+
# @param [String] id Unique identifier of a site.
|
494
|
+
# @return [Site] The requested site, if found.
|
495
|
+
#
|
496
|
+
def self.load(nsc, id)
|
497
|
+
uri = "/api/2.1/site_configurations/#{id}"
|
498
|
+
resp = AJAX.get(nsc, uri, AJAX::CONTENT_TYPE::JSON)
|
499
|
+
hash = JSON.parse(resp, symbolize_names: true)
|
500
|
+
site = self.json_initializer(hash).deserialize(hash)
|
501
|
+
|
502
|
+
# Convert each string address to either a HostName or IPRange object
|
503
|
+
included_addresses = hash[:included_scan_targets][:addresses]
|
504
|
+
site.included_scan_targets[:addresses] = []
|
505
|
+
included_addresses.each { |asset| site.include_asset(asset) }
|
506
|
+
|
507
|
+
excluded_addresses = hash[:excluded_scan_targets][:addresses]
|
508
|
+
site.excluded_scan_targets[:addresses] = []
|
509
|
+
excluded_addresses.each { |asset| site.exclude_asset(asset) }
|
510
|
+
|
511
|
+
site.organization = Organization.create(site.organization)
|
512
|
+
site.schedules = (hash[:schedules] || []).map {|schedule| Nexpose::Schedule.from_hash(schedule) }
|
513
|
+
site.site_credentials = hash[:site_credentials].map {|cred| Nexpose::SiteCredentials.new.object_from_hash(nsc,cred)}
|
514
|
+
site.shared_credentials = hash[:shared_credentials].map {|cred| Nexpose::SiteCredentials.new.object_from_hash(nsc,cred)}
|
515
|
+
site.discovery_config = Nexpose::DiscoveryConnection.new.object_from_hash(nsc, hash[:discovery_config]) unless hash[:discovery_config].nil?
|
516
|
+
site.search_criteria = Nexpose::DiscoveryConnection::Criteria.parseHash(hash[:search_criteria]) unless hash[:search_criteria].nil?
|
517
|
+
site.alerts = Alert.load_alerts(hash[:alerts])
|
518
|
+
site.tags = Tag.load_tags(hash[:tags])
|
519
|
+
site.web_credentials = hash[:web_credentials].map {|webCred| (
|
520
|
+
webCred[:service] == Nexpose::WebCredentials::WebAppAuthType::HTTP_HEADER ?
|
521
|
+
Nexpose::WebCredentials::Headers.new(webCred[:name], webCred[:baseURL], webCred[:soft403Pattern], webCred[:id]).object_from_hash(nsc,webCred) :
|
522
|
+
Nexpose::WebCredentials::HTMLForms.new(webCred[:name], webCred[:baseURL], webCred[:loginURL], webCred[:soft403Pattern], webCred[:id]).object_from_hash(nsc,webCred))}
|
523
|
+
|
524
|
+
site
|
525
|
+
end
|
526
|
+
|
527
|
+
def self.json_initializer(data)
|
528
|
+
new(data[:name], data[:scan_template_id])
|
529
|
+
end
|
530
|
+
|
313
531
|
# Copy an existing configuration from a Nexpose instance.
|
314
532
|
# Returned object will reset the site ID and append "Copy" to the existing
|
315
533
|
# name.
|
@@ -333,23 +551,21 @@ module Nexpose
|
|
333
551
|
# @return [Fixnum] Site ID assigned to this configuration, if successful.
|
334
552
|
#
|
335
553
|
def save(connection)
|
336
|
-
|
337
|
-
raise APIError.new(nil, 'Cannot save a dynamic site without a discovery connection configured.') unless @discovery_connection_id
|
338
|
-
|
339
|
-
new_site = @id == -1
|
340
|
-
save_dynamic_criteria(connection) if new_site
|
341
|
-
|
342
|
-
# Have to retrieve and attach shared creds, or saving will fail.
|
343
|
-
xml = _append_shared_creds_to_xml(connection, as_xml)
|
344
|
-
response = AJAX.post(connection, '/data/site/config', xml)
|
345
|
-
saved = REXML::XPath.first(REXML::Document.new(response), 'ajaxResponse')
|
346
|
-
raise APIError.new(response, 'Failed to save dynamic site.') if saved.nil? || saved.attributes['success'].to_i != 1
|
554
|
+
new_site = @id == -1
|
347
555
|
|
348
|
-
|
556
|
+
if new_site
|
557
|
+
resp = AJAX.post(connection, '/api/2.1/site_configurations/', to_json, AJAX::CONTENT_TYPE::JSON)
|
558
|
+
@id = resp.to_i
|
349
559
|
else
|
350
|
-
|
351
|
-
@id = r.attributes['site-id'].to_i if r.success
|
560
|
+
resp = AJAX.put(connection, "/api/2.1/site_configurations/#{@id}", to_json, AJAX::CONTENT_TYPE::JSON)
|
352
561
|
end
|
562
|
+
|
563
|
+
# Retrieve the scan engine and shared credentials and add them to the site configuration
|
564
|
+
site_config = Site.load(connection, @id)
|
565
|
+
@engine_id = site_config.engine_id
|
566
|
+
@shared_credentials = site_config.shared_credentials
|
567
|
+
@alerts = site_config.alerts
|
568
|
+
|
353
569
|
@id
|
354
570
|
end
|
355
571
|
|
@@ -378,209 +594,6 @@ module Nexpose
|
|
378
594
|
response = connection.execute(xml, '1.1', timeout: 60)
|
379
595
|
Scan.parse(response.res) if response.success
|
380
596
|
end
|
381
|
-
|
382
|
-
# Save only the criteria of a dynamic site.
|
383
|
-
#
|
384
|
-
# @param [Connection] nsc Connection to a console.
|
385
|
-
# @return [Fixnum] Site ID.
|
386
|
-
#
|
387
|
-
def save_dynamic_criteria(nsc)
|
388
|
-
# Several parameters are passed through the URI
|
389
|
-
params = { 'configID' => @discovery_connection_id,
|
390
|
-
'entityid' => @id > 0 ? @id : false,
|
391
|
-
'mode' => @id > 0 ? 'edit' : false }
|
392
|
-
uri = AJAX.parameterize_uri('/data/site/saveSite', params)
|
393
|
-
|
394
|
-
# JSON body of POST request contains details.
|
395
|
-
details = { 'dynamic' => true,
|
396
|
-
'name' => @name,
|
397
|
-
'tag' => @description.nil? ? '' : @description,
|
398
|
-
'riskFactor' => @risk_factor,
|
399
|
-
# 'vCenter' => @discovery_connection_id,
|
400
|
-
'searchCriteria' => @criteria.nil? ? { 'operator' => 'AND' } : @criteria.to_h }
|
401
|
-
json = JSON.generate(details)
|
402
|
-
|
403
|
-
response = AJAX.post(nsc, uri, json, AJAX::CONTENT_TYPE::JSON)
|
404
|
-
json = JSON.parse(response)
|
405
|
-
if json['response'] =~ /success/
|
406
|
-
if @id < 1
|
407
|
-
@id = json['entityID'].to_i
|
408
|
-
end
|
409
|
-
else
|
410
|
-
raise APIError.new(response, json['message'])
|
411
|
-
end
|
412
|
-
@id
|
413
|
-
end
|
414
|
-
|
415
|
-
# Retrieve the currrent filter criteria used by a dynamic site.
|
416
|
-
#
|
417
|
-
# @param [Connection] nsc Connection to a console.
|
418
|
-
# @return [Criteria] Current criteria for the site.
|
419
|
-
#
|
420
|
-
def load_dynamic_attributes(nsc)
|
421
|
-
response = AJAX.get(nsc, "/data/site/loadDynamicSite?entityid=#{@id}")
|
422
|
-
json = JSON.parse(response)
|
423
|
-
@discovery_connection_id = json['discoveryConfigs']['id']
|
424
|
-
@criteria = Criteria.parse(json['searchCriteria'])
|
425
|
-
end
|
426
|
-
|
427
|
-
include Sanitize
|
428
|
-
|
429
|
-
# Generate an XML representation of this site configuration
|
430
|
-
#
|
431
|
-
# @return [String] XML valid for submission as part of other requests.
|
432
|
-
#
|
433
|
-
def as_xml
|
434
|
-
xml = REXML::Element.new('Site')
|
435
|
-
xml.attributes['id'] = @id
|
436
|
-
xml.attributes['name'] = @name
|
437
|
-
xml.attributes['description'] = @description
|
438
|
-
xml.attributes['riskfactor'] = @risk_factor
|
439
|
-
xml.attributes['isDynamic'] = '1' if dynamic?
|
440
|
-
# TODO This should be set to 'Amazon Web Services' for AWS.
|
441
|
-
xml.attributes['dynamicConfigType'] = 'vSphere' if dynamic?
|
442
|
-
|
443
|
-
if @description && !@description.empty?
|
444
|
-
elem = REXML::Element.new('Description')
|
445
|
-
elem.add_text(@description)
|
446
|
-
xml.add_element(elem)
|
447
|
-
end
|
448
|
-
|
449
|
-
unless @users.empty?
|
450
|
-
elem = REXML::Element.new('Users')
|
451
|
-
@users.each { |user| elem.add_element('user', { 'id' => user }) }
|
452
|
-
xml.add_element(elem)
|
453
|
-
end
|
454
|
-
|
455
|
-
xml.add_element(@organization.as_xml) if @organization
|
456
|
-
|
457
|
-
elem = REXML::Element.new('Hosts')
|
458
|
-
@assets.each { |a| elem.add_element(a.as_xml) }
|
459
|
-
xml.add_element(elem)
|
460
|
-
|
461
|
-
elem = REXML::Element.new('ExcludedHosts')
|
462
|
-
@exclude.each { |e| elem.add_element(e.as_xml) }
|
463
|
-
xml.add_element(elem)
|
464
|
-
|
465
|
-
unless credentials.empty?
|
466
|
-
elem = REXML::Element.new('Credentials')
|
467
|
-
@credentials.each { |c| elem.add_element(c.as_xml) }
|
468
|
-
xml.add_element(elem)
|
469
|
-
end
|
470
|
-
|
471
|
-
unless alerts.empty?
|
472
|
-
elem = REXML::Element.new('Alerting')
|
473
|
-
alerts.each { |a| elem.add_element(a.as_xml) }
|
474
|
-
xml.add_element(elem)
|
475
|
-
end
|
476
|
-
|
477
|
-
elem = REXML::Element.new('ScanConfig')
|
478
|
-
elem.add_attributes({ 'configID' => @id,
|
479
|
-
'name' => @scan_template_name || @scan_template,
|
480
|
-
'templateID' => @scan_template,
|
481
|
-
'configVersion' => @config_version || 3,
|
482
|
-
'engineID' => @engine })
|
483
|
-
sched = REXML::Element.new('Schedules')
|
484
|
-
@schedules.each { |s| sched.add_element(s.as_xml) }
|
485
|
-
elem.add_element(sched)
|
486
|
-
xml.add_element(elem)
|
487
|
-
|
488
|
-
unless tags.empty?
|
489
|
-
tag_xml = xml.add_element(REXML::Element.new('Tags'))
|
490
|
-
@tags.each { |tag| tag_xml.add_element(tag.as_xml) }
|
491
|
-
end
|
492
|
-
|
493
|
-
xml
|
494
|
-
end
|
495
|
-
|
496
|
-
def to_xml
|
497
|
-
as_xml.to_s
|
498
|
-
end
|
499
|
-
|
500
|
-
# Parse a response from a Nexpose console into a valid Site object.
|
501
|
-
#
|
502
|
-
# @param [REXML::Document] rexml XML document to parse.
|
503
|
-
# @return [Site] Site object represented by the XML.
|
504
|
-
# ## TODO What is returned on failure?
|
505
|
-
#
|
506
|
-
def self.parse(rexml)
|
507
|
-
rexml.elements.each('//Site') do |s|
|
508
|
-
site = Site.new(s.attributes['name'])
|
509
|
-
site.id = s.attributes['id'].to_i
|
510
|
-
site.description = s.attributes['description']
|
511
|
-
site.risk_factor = s.attributes['riskfactor'] || 1.0
|
512
|
-
site.is_dynamic = true if s.attributes['isDynamic'] == '1'
|
513
|
-
|
514
|
-
s.elements.each('Description') do |desc|
|
515
|
-
site.description = desc.text
|
516
|
-
end
|
517
|
-
|
518
|
-
s.elements.each('Users/user') do |user|
|
519
|
-
site.users << user.attributes['id'].to_i
|
520
|
-
end
|
521
|
-
|
522
|
-
s.elements.each('Organization') do |org|
|
523
|
-
site.organization = Organization.parse(org)
|
524
|
-
end
|
525
|
-
|
526
|
-
s.elements.each('Hosts/range') do |r|
|
527
|
-
site.assets << IPRange.new(r.attributes['from'], r.attributes['to'])
|
528
|
-
end
|
529
|
-
s.elements.each('Hosts/host') do |host|
|
530
|
-
site.assets << HostName.new(host.text)
|
531
|
-
end
|
532
|
-
|
533
|
-
s.elements.each('ExcludedHosts/range') do |r|
|
534
|
-
site.exclude << IPRange.new(r.attributes['from'], r.attributes['to'])
|
535
|
-
end
|
536
|
-
s.elements.each('ExcludedHosts/host') do |host|
|
537
|
-
site.exclude << HostName.new(host.text)
|
538
|
-
end
|
539
|
-
|
540
|
-
s.elements.each('Credentials/adminCredentials') do |cred|
|
541
|
-
site.credentials << SiteCredential.parse(cred)
|
542
|
-
end
|
543
|
-
|
544
|
-
s.elements.each('ScanConfig') do |scan_config|
|
545
|
-
site.scan_template_name = scan_config.attributes['name']
|
546
|
-
site.scan_template = scan_config.attributes['templateID']
|
547
|
-
site.config_version = scan_config.attributes['configVersion'].to_i
|
548
|
-
site.engine = scan_config.attributes['engineID'].to_i
|
549
|
-
scan_config.elements.each('Schedules/Schedule') do |schedule|
|
550
|
-
site.schedules << Schedule.parse(schedule)
|
551
|
-
end
|
552
|
-
end
|
553
|
-
|
554
|
-
s.elements.each('Alerting/Alert') do |alert|
|
555
|
-
site.alerts << Alert.parse(alert)
|
556
|
-
end
|
557
|
-
|
558
|
-
s.elements.each('Tags/Tag') do |tag|
|
559
|
-
site.tags << TagSummary.parse_xml(tag)
|
560
|
-
end
|
561
|
-
|
562
|
-
return site
|
563
|
-
end
|
564
|
-
nil
|
565
|
-
end
|
566
|
-
|
567
|
-
def _append_shared_creds_to_xml(connection, xml)
|
568
|
-
xml_w_creds = AJAX.get(connection, "/data/site/config?siteid=#{@id}")
|
569
|
-
cred_xml = REXML::XPath.first(REXML::Document.new(xml_w_creds), 'Site/Credentials')
|
570
|
-
unless cred_xml.nil?
|
571
|
-
creds = REXML::XPath.first(xml, 'Credentials')
|
572
|
-
if creds.nil?
|
573
|
-
xml.add_element(cred_xml)
|
574
|
-
else
|
575
|
-
cred_xml.elements.each do |cred|
|
576
|
-
if cred.attributes['shared'].to_i == 1
|
577
|
-
creds.add_element(cred)
|
578
|
-
end
|
579
|
-
end
|
580
|
-
end
|
581
|
-
end
|
582
|
-
xml
|
583
|
-
end
|
584
597
|
end
|
585
598
|
|
586
599
|
# Object that represents the summary of a Nexpose Site.
|
@@ -642,6 +655,10 @@ module Nexpose
|
|
642
655
|
def to_xml
|
643
656
|
to_xml_elem.to_s
|
644
657
|
end
|
658
|
+
|
659
|
+
def to_s
|
660
|
+
@host.to_s
|
661
|
+
end
|
645
662
|
end
|
646
663
|
|
647
664
|
# Object that represents a single IP address or an inclusive range of IP addresses.
|
@@ -752,5 +769,10 @@ module Nexpose
|
|
752
769
|
def to_xml
|
753
770
|
as_xml.to_s
|
754
771
|
end
|
772
|
+
|
773
|
+
def to_s
|
774
|
+
return from.to_s if to.nil?
|
775
|
+
"#{from.to_s} - #{to.to_s}"
|
776
|
+
end
|
755
777
|
end
|
756
778
|
end
|