nexpose 0.9.8 → 1.0.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/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
|