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
data/lib/nexpose/discovery.rb
CHANGED
@@ -27,20 +27,22 @@ module Nexpose
|
|
27
27
|
end
|
28
28
|
end
|
29
29
|
|
30
|
-
class DiscoveryConnection
|
30
|
+
class DiscoveryConnection < APIObject
|
31
31
|
include XMLUtils
|
32
32
|
|
33
33
|
module Protocol
|
34
|
-
HTTP
|
34
|
+
HTTP = 'HTTP'
|
35
35
|
HTTPS = 'HTTPS'
|
36
|
-
LDAP
|
36
|
+
LDAP = 'LDAP'
|
37
37
|
LDAPS = 'LDAPS'
|
38
38
|
end
|
39
39
|
|
40
40
|
module Type
|
41
|
-
VSPHERE
|
42
|
-
AWS
|
43
|
-
ACTIVESYNC
|
41
|
+
VSPHERE = 'VSPHERE'
|
42
|
+
AWS = 'AWS'
|
43
|
+
ACTIVESYNC = 'ACTIVESYNC'
|
44
|
+
ACTIVESYNC_POWERSHELL = 'ACTIVESYNC_POWERSHELL'
|
45
|
+
ACTIVESYNC_OFFICE365 = 'ACTIVESYNC_OFFICE365'
|
44
46
|
end
|
45
47
|
|
46
48
|
# A unique identifier for this connection.
|
@@ -55,18 +57,30 @@ module Nexpose
|
|
55
57
|
# The IP address or fully qualified domain name of the server.
|
56
58
|
attr_accessor :address
|
57
59
|
|
60
|
+
# The engine ID to use for this connection.
|
61
|
+
attr_accessor :engine_id
|
62
|
+
|
58
63
|
# A user name that can be used to log into the server.
|
59
64
|
attr_accessor :user
|
60
65
|
|
61
66
|
# The password to use when connecting with the defined user.
|
62
67
|
attr_accessor :password
|
63
68
|
|
64
|
-
# The protocol used for
|
69
|
+
# The protocol used for connecting to the server. One of DiscoveryConnection::Protocol
|
65
70
|
attr_accessor :protocol
|
66
71
|
|
67
72
|
# The port used for connecting to the server. A valid port from 1 to 65535.
|
68
73
|
attr_accessor :port
|
69
74
|
|
75
|
+
# The hostname of the exchange server to connect for exchange powershell connections
|
76
|
+
attr_accessor :exchange_hostname
|
77
|
+
|
78
|
+
# The exchange username to connect for exchange powershell connections
|
79
|
+
attr_accessor :exchange_username
|
80
|
+
|
81
|
+
# The exchange password to connect for exchange powershell connections
|
82
|
+
attr_accessor :exchange_password
|
83
|
+
|
70
84
|
# Whether or not the connection is active.
|
71
85
|
# Discovery is only possible when the connection is active.
|
72
86
|
attr_accessor :status
|
@@ -79,7 +93,7 @@ module Nexpose
|
|
79
93
|
# @param [String] user User name for credentials on this connection.
|
80
94
|
# @param [String] password Password for credentials on this connection.
|
81
95
|
#
|
82
|
-
def initialize(name, address, user, password = nil)
|
96
|
+
def initialize(name = nil, address = nil, user = nil, password = nil)
|
83
97
|
@name, @address, @user, @password = name, address, user, password
|
84
98
|
@type = nil # for backwards compatibilitly, at some point should set this to Type::VSPHERE
|
85
99
|
@id = -1
|
@@ -143,13 +157,17 @@ module Nexpose
|
|
143
157
|
|
144
158
|
def as_xml
|
145
159
|
xml = REXML::Element.new('DiscoveryConnection')
|
146
|
-
xml.attributes['name']
|
147
|
-
xml.attributes['address']
|
148
|
-
xml.attributes['port']
|
149
|
-
xml.attributes['protocol']
|
150
|
-
xml.attributes['user-name']
|
151
|
-
xml.attributes['password']
|
152
|
-
xml.attributes['
|
160
|
+
xml.attributes['name'] = @name
|
161
|
+
xml.attributes['address'] = @address
|
162
|
+
xml.attributes['port'] = @port
|
163
|
+
xml.attributes['protocol'] = @protocol
|
164
|
+
xml.attributes['user-name'] = @user
|
165
|
+
xml.attributes['password'] = @password
|
166
|
+
xml.attributes['exchange-hostname'] = @exchange_hostname if @exchange_hostname
|
167
|
+
xml.attributes['exchange-username'] = @exchange_username if @exchange_username
|
168
|
+
xml.attributes['exchange-password'] = @exchange_password if @exchange_password
|
169
|
+
xml.attributes['type'] = @type if @type
|
170
|
+
xml.attributes['engine-id'] = @engine_id if @engine_id && @engine_id != -1
|
153
171
|
xml
|
154
172
|
end
|
155
173
|
|
@@ -167,6 +185,70 @@ module Nexpose
|
|
167
185
|
conn.status = xml.attributes['connection-status']
|
168
186
|
conn
|
169
187
|
end
|
188
|
+
|
189
|
+
def to_json
|
190
|
+
JSON.generate(to_h)
|
191
|
+
end
|
192
|
+
|
193
|
+
def to_h
|
194
|
+
{ id: id,
|
195
|
+
name: name,
|
196
|
+
type: type
|
197
|
+
# TODO Add remaining instance fields, once it is introduced in resource object
|
198
|
+
}
|
199
|
+
end
|
200
|
+
|
201
|
+
def ==(other)
|
202
|
+
eql?(other)
|
203
|
+
end
|
204
|
+
|
205
|
+
def eql?(other)
|
206
|
+
id.eql?(other.id) &&
|
207
|
+
name.eql?(other.name) &&
|
208
|
+
type.eql?(other.type)
|
209
|
+
# TODO Add remaining instance fields, once it is introduced in resource object
|
210
|
+
end
|
211
|
+
|
212
|
+
# Override of filter criterion to account for proper JSON naming.
|
213
|
+
#
|
214
|
+
class Criterion < Nexpose::Criterion
|
215
|
+
# Convert to Hash, which can be converted to JSON for API calls.
|
216
|
+
def to_h
|
217
|
+
{ operator: operator,
|
218
|
+
values: Array(value),
|
219
|
+
field_name: field }
|
220
|
+
end
|
221
|
+
|
222
|
+
# Create a Criterion object from a JSON-derived Hash.
|
223
|
+
#
|
224
|
+
# @param [Hash] json JSON-derived Hash of a Criterion object.
|
225
|
+
# @return [Criterion] Parsed object.
|
226
|
+
#
|
227
|
+
def self.parseHash(hash)
|
228
|
+
Criterion.new(hash[:field_name],
|
229
|
+
hash[:operator],
|
230
|
+
hash[:values])
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
# Override of filter criteria to account for different parsing from JSON.
|
235
|
+
#
|
236
|
+
class Criteria < Nexpose::Criteria
|
237
|
+
# Create a Criteria object from a Hash.
|
238
|
+
#
|
239
|
+
# @param [Hash] Hash of a Criteria object.
|
240
|
+
# @return [Criteria] Parsed object.
|
241
|
+
#
|
242
|
+
def self.parseHash(hash)
|
243
|
+
# The call returns empty JSON, so default to 'AND' if not present.
|
244
|
+
operator = hash[:operator] || 'AND'
|
245
|
+
ret = Criteria.new([], operator)
|
246
|
+
hash[:criteria].each do |c|
|
247
|
+
ret.criteria << Criterion.parseHash(c)
|
248
|
+
end
|
249
|
+
ret
|
250
|
+
end
|
251
|
+
end
|
170
252
|
end
|
171
253
|
|
172
254
|
class DiscoveredAsset
|
@@ -216,8 +298,51 @@ module Nexpose
|
|
216
298
|
@name, @protocol, @address, @user, @password = name, protocol, address, user, password
|
217
299
|
@type = Type::ACTIVESYNC
|
218
300
|
@id = -1
|
219
|
-
@port = 443
|
301
|
+
@port = 443 # port not used for mobile connection
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
class MobilePowershellDiscoveryConnection < DiscoveryConnection
|
306
|
+
# Create a new Mobile Powershell discovery connection.
|
307
|
+
#
|
308
|
+
# @param [String] name Name to assign to this connection.
|
309
|
+
# @param [String] address IP or fully qualified domain name of the
|
310
|
+
# WinRM server.
|
311
|
+
# @param [String] user WinRM User name for credentials on this connection.
|
312
|
+
# @param [String] password WinRM password for credentials on this connection.
|
313
|
+
# @param [String] exchange_hostname fully qualified domain name of the exchange server
|
314
|
+
# @param [String] exchange_username Exchange User name for exchange credentials on this connection.
|
315
|
+
# @param [String] exchange_password Exchange password for exchange credentials on this connection.
|
316
|
+
#
|
317
|
+
def initialize(name, address, user, password, exchange_hostname, exchange_username, exchange_password)
|
318
|
+
@name, @address, @user, @password = name, address, user, password
|
319
|
+
@protocol = Protocol::HTTPS
|
320
|
+
@exchange_hostname, @exchange_username, @exchange_password = exchange_hostname, exchange_username, exchange_password
|
321
|
+
@type = Type::ACTIVESYNC_POWERSHELL
|
322
|
+
@id = -1
|
323
|
+
@port = 443 # port not used for mobile connection
|
220
324
|
end
|
325
|
+
end
|
221
326
|
|
327
|
+
class MobileOffice365DiscoveryConnection < DiscoveryConnection
|
328
|
+
# Create a new Mobile Office365 discovery connection.
|
329
|
+
#
|
330
|
+
# @param [String] name Name to assign to this connection.
|
331
|
+
# @param [String] address IP or fully qualified domain name of the
|
332
|
+
# WinRM server.
|
333
|
+
# @param [String] user WinRM User name for credentials on this connection.
|
334
|
+
# @param [String] password WinRM password for credentials on this connection.
|
335
|
+
# @param [String] exchange_username Exchange User name for exchange credentials on this connection.
|
336
|
+
# @param [String] exchange_password Exchange password for exchange credentials on this connection.
|
337
|
+
#
|
338
|
+
def initialize(name, address, user, password, exchange_username, exchange_password)
|
339
|
+
@name, @address, @user, @password = name, address, user, password
|
340
|
+
@protocol = Protocol::HTTPS
|
341
|
+
@exchange_hostname = '' # nexpose will set to office365 server
|
342
|
+
@exchange_username, @exchange_password = exchange_username, exchange_password
|
343
|
+
@type = Type::ACTIVESYNC_OFFICE365
|
344
|
+
@id = -1
|
345
|
+
@port = 443 # port not used for mobile connection
|
346
|
+
end
|
222
347
|
end
|
223
348
|
end
|
@@ -10,10 +10,10 @@ module Nexpose
|
|
10
10
|
DATACENTER ='DATACENTER'
|
11
11
|
|
12
12
|
# Valid Operators: CONTAINS, NOT_CONTAINS
|
13
|
-
GUEST_OS_FAMILY = 'GUEST_OS_FAMILY'
|
13
|
+
GUEST_OS_FAMILY = 'GUEST_OS_FAMILY' #Also AWS Filter
|
14
14
|
|
15
15
|
# Valid Operators: IN, NOT_IN
|
16
|
-
IP_ADDRESS_RANGE = 'IP_ADDRESS'
|
16
|
+
IP_ADDRESS_RANGE = 'IP_ADDRESS' #Also AWS Filter
|
17
17
|
|
18
18
|
# Valid Operators: IN, NOT_IN
|
19
19
|
# Valid Values (See Value::PowerState): ON, OFF, SUSPENDED
|
@@ -25,13 +25,36 @@ module Nexpose
|
|
25
25
|
# Valid Operators: IS, IS_NOT, CONTAINS, NOT_CONTAINS, STARTS_WITH
|
26
26
|
VIRTUAL_MACHINE_NAME = 'VM'
|
27
27
|
|
28
|
-
|
28
|
+
# valid Operators: IS, IS_NOT, CONTAINS, NOT_CONTAINS, STARTS_WITH
|
29
|
+
HOST = 'HOST_NAME'
|
30
|
+
|
31
|
+
###### AWS Filters ######
|
32
|
+
# Valid Operators: CONTAINS, NOT_CONTAINS
|
33
|
+
AVAILABILITY_ZONE = 'AVAILABILITY_ZONE'
|
34
|
+
|
35
|
+
# Valid Operators: CONTAINS, NOT_CONTAINS
|
36
|
+
INSTANCE_ID = 'INSTANCE_ID'
|
37
|
+
|
38
|
+
# Valid Operators: IS, IS_NOT, CONTAINS, NOT_CONTAINS, STARTS_WITH
|
39
|
+
INSTANCE_NAME = 'INSTANCE_NAME'
|
40
|
+
|
41
|
+
# Valid Operators: IN, NOT_IN
|
42
|
+
INSTANCE_STATE = 'INSTANCE_STATE'
|
43
|
+
|
44
|
+
# Valid Operators: IN, NOT_IN
|
45
|
+
INSTANCE_TYPE = 'INSTANCE_TYPE'
|
46
|
+
|
47
|
+
# Valid Operators: IN, NOT_IN
|
48
|
+
REGION ='REGION'
|
49
|
+
|
50
|
+
###### Mobile or Active sync Filters ######
|
29
51
|
# Valid Operators: CONTAINS, NOT_CONTAINS
|
30
52
|
OPERATING_SYSTEM = 'DEVICE_OPERATING_SYSTEM'
|
31
53
|
|
32
54
|
# Valid Operators: IS, IS_NOT, CONTAINS, NOT_CONTAINS, STARTS_WITH
|
33
55
|
USER = 'DEVICE_USER_DISPLAY_NAME'
|
34
56
|
|
57
|
+
|
35
58
|
end
|
36
59
|
|
37
60
|
module Value
|
data/lib/nexpose/engine.rb
CHANGED
@@ -16,6 +16,22 @@ module Nexpose
|
|
16
16
|
response.success
|
17
17
|
end
|
18
18
|
|
19
|
+
# Reverses the direction of a connection to an engine
|
20
|
+
# If the connection is currently initiated from the console this method
|
21
|
+
# will have the engine initiate the connection. If the connection is
|
22
|
+
# currently initiated by the engine this method with initiate the connection
|
23
|
+
# from the console instead. Requires a restart of the console for the
|
24
|
+
# connection to be properly established.
|
25
|
+
#
|
26
|
+
# @param [Fixnum] engine_id Unique ID of the engine.
|
27
|
+
# @return [Boolean] true if the connection is successfully reversed.
|
28
|
+
#
|
29
|
+
def reverse_engine_connection(engine_id)
|
30
|
+
uri = "/api/2.1/engine/#{engine_id}/reverseConnection"
|
31
|
+
response = AJAX.put(self, uri)
|
32
|
+
response.eql?("true")
|
33
|
+
end
|
34
|
+
|
19
35
|
# Provide a list of current scan activities for a specific Scan Engine.
|
20
36
|
#
|
21
37
|
# @return [Array[ScanSummary]] Array of ScanSummary objects associated with
|
@@ -0,0 +1,92 @@
|
|
1
|
+
module Nexpose
|
2
|
+
module JsonSerializer
|
3
|
+
@@namespace = 'Nexpose'
|
4
|
+
|
5
|
+
def deserialize(data)
|
6
|
+
data.each do |key, value|
|
7
|
+
if respond_to?(key)
|
8
|
+
property = value
|
9
|
+
|
10
|
+
if value.respond_to? :each
|
11
|
+
obj = resolve_type(key)
|
12
|
+
|
13
|
+
unless obj.nil?
|
14
|
+
if value.is_a?(Array)
|
15
|
+
property = value.map { |dv| ((dv.respond_to? :each) ? create_object(obj, dv).deserialize(dv): dv) }
|
16
|
+
else
|
17
|
+
property = create_object(obj, value).deserialize(value)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
elsif value.is_a?(String) && value.match(/^\d{8}T\d{6}\.\d{3}/)
|
21
|
+
property = ISO8601.to_time(value)
|
22
|
+
end
|
23
|
+
|
24
|
+
instance_variable_set("@#{key}", property)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
self
|
29
|
+
end
|
30
|
+
|
31
|
+
def serialize()
|
32
|
+
hash = to_hash(Hash.new)
|
33
|
+
|
34
|
+
unless hash.nil?
|
35
|
+
JSON.generate(hash)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def to_hash(hash)
|
40
|
+
self.instance_variables.each do |m|
|
41
|
+
value = self.instance_variable_get(m)
|
42
|
+
hash[m.to_s.delete('@')] = do_hash(value)
|
43
|
+
end
|
44
|
+
|
45
|
+
hash
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def do_hash(obj)
|
51
|
+
if obj.is_a?(Array)
|
52
|
+
obj = obj.map do |el|
|
53
|
+
do_hash(el)
|
54
|
+
|
55
|
+
end
|
56
|
+
elsif obj.class.included_modules.include? JsonSerializer
|
57
|
+
obj = obj.to_hash(Hash.new)
|
58
|
+
end
|
59
|
+
|
60
|
+
obj
|
61
|
+
end
|
62
|
+
|
63
|
+
def create_object(obj, data)
|
64
|
+
if obj.respond_to?(:json_initializer)
|
65
|
+
obj.method(:json_initializer).call(data)
|
66
|
+
else
|
67
|
+
obj.method(:new).call
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def resolve_type(field)
|
72
|
+
class_name = normalize_field(field)
|
73
|
+
type_attribute = "#{field}_type"
|
74
|
+
|
75
|
+
if self.respond_to?(type_attribute)
|
76
|
+
clazz = self.public_send(type_attribute)
|
77
|
+
elsif Object.const_get(@@namespace).const_defined?(class_name)
|
78
|
+
resolved = Object.const_get(@@namespace).const_get(class_name)
|
79
|
+
clazz = resolved if resolved.included_modules.include? JsonSerializer
|
80
|
+
end
|
81
|
+
|
82
|
+
clazz
|
83
|
+
end
|
84
|
+
|
85
|
+
def normalize_field(field)
|
86
|
+
class_name = field.to_s.split('_').map(&:capitalize!).join
|
87
|
+
class_name = 'Vulnerability' if class_name == 'Vulnerabilities'
|
88
|
+
class_name.chop! if class_name.end_with?('s')
|
89
|
+
class_name
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
data/lib/nexpose/scan.rb
CHANGED
@@ -11,6 +11,16 @@ module Nexpose
|
|
11
11
|
scan_devices([device])
|
12
12
|
end
|
13
13
|
|
14
|
+
# Perform an ad hoc scan of a single device at a specific time.
|
15
|
+
#
|
16
|
+
# @param [Device] device Device to scan.
|
17
|
+
# @param [Array[adhoc_schedules]] list of scheduled times at which to run
|
18
|
+
# @return [Status] whether the request was successful
|
19
|
+
#
|
20
|
+
def scan_device_with_schedule(device, schedule)
|
21
|
+
scan_devices_with_schedule([device], schedule)
|
22
|
+
end
|
23
|
+
|
14
24
|
# Perform an ad hoc scan of a subset of devices for a site.
|
15
25
|
# Nexpose only allows devices from a single site to be submitted per
|
16
26
|
# request.
|
@@ -24,17 +34,45 @@ module Nexpose
|
|
24
34
|
# @return [Scan] Scan launch information.
|
25
35
|
#
|
26
36
|
def scan_devices(devices)
|
27
|
-
site_id = devices.map
|
28
|
-
xml = make_xml('SiteDevicesScanRequest',
|
37
|
+
site_id = devices.map(&:site_id).uniq.first
|
38
|
+
xml = make_xml('SiteDevicesScanRequest', 'site-id' => site_id)
|
29
39
|
elem = REXML::Element.new('Devices')
|
30
40
|
devices.each do |device|
|
31
|
-
elem.add_element('device',
|
41
|
+
elem.add_element('device', 'id' => "#{device.id}")
|
32
42
|
end
|
33
43
|
xml.add_element(elem)
|
34
44
|
|
35
45
|
_scan_ad_hoc(xml)
|
36
46
|
end
|
37
47
|
|
48
|
+
# Perform an ad hoc scan of a subset of devices for a site.
|
49
|
+
# Nexpose only allows devices from a single site to be submitted per
|
50
|
+
# request.
|
51
|
+
# Method is designed to take objects from a Device listing.
|
52
|
+
#
|
53
|
+
# For example:
|
54
|
+
# devices = nsc.devices(5)
|
55
|
+
# nsc.scan_devices(devices.take(10))
|
56
|
+
#
|
57
|
+
# @param [Array[Device]] devices List of devices to scan.
|
58
|
+
# @param [Array[adhoc_schedules]] list of scheduled times at which to run
|
59
|
+
# @return [Status] whether the request was successful
|
60
|
+
#
|
61
|
+
def scan_devices_with_schedule(devices, schedules)
|
62
|
+
site_id = devices.map(&:site_id).uniq.first
|
63
|
+
xml = make_xml('SiteDevicesScanRequest', 'site-id' => site_id)
|
64
|
+
elem = REXML::Element.new('Devices')
|
65
|
+
devices.each do |device|
|
66
|
+
elem.add_element('device', 'id' => "#{device.id}")
|
67
|
+
end
|
68
|
+
xml.add_element(elem)
|
69
|
+
scheds = REXML::Element.new('Schedules')
|
70
|
+
schedules.each { |sched| scheds.add_element(sched.as_xml) }
|
71
|
+
xml.add_element(scheds)
|
72
|
+
|
73
|
+
_scan_ad_hoc_with_schedules(xml)
|
74
|
+
end
|
75
|
+
|
38
76
|
# Perform an ad hoc scan of a single asset of a site.
|
39
77
|
#
|
40
78
|
# @param [Fixnum] site_id Site ID that the assets belong to.
|
@@ -45,6 +83,17 @@ module Nexpose
|
|
45
83
|
scan_assets(site_id, [asset])
|
46
84
|
end
|
47
85
|
|
86
|
+
# Perform an ad hoc scan of a single asset of a site at a specific time
|
87
|
+
#
|
88
|
+
# @param [Fixnum] site_id Site ID that the assets belong to.
|
89
|
+
# @param [HostName|IPRange] asset Asset to scan.
|
90
|
+
# @param [Array[adhoc_schedules]] list of scheduled times at which to run
|
91
|
+
# @return [Status] whether the request was successful
|
92
|
+
#
|
93
|
+
def scan_asset_with_schedule(site_id, asset, schedule)
|
94
|
+
scan_assets_with_schedule(site_id, [asset], schedule)
|
95
|
+
end
|
96
|
+
|
48
97
|
# Perform an ad hoc scan of a subset of assets for a site.
|
49
98
|
# Only assets from a single site should be submitted per request.
|
50
99
|
# Method is designed to take objects filtered from Site#assets.
|
@@ -58,7 +107,7 @@ module Nexpose
|
|
58
107
|
# @return [Scan] Scan launch information.
|
59
108
|
#
|
60
109
|
def scan_assets(site_id, assets)
|
61
|
-
xml = make_xml('SiteDevicesScanRequest',
|
110
|
+
xml = make_xml('SiteDevicesScanRequest', 'site-id' => site_id)
|
62
111
|
hosts = REXML::Element.new('Hosts')
|
63
112
|
assets.each { |asset| _append_asset!(hosts, asset) }
|
64
113
|
xml.add_element(hosts)
|
@@ -66,6 +115,59 @@ module Nexpose
|
|
66
115
|
_scan_ad_hoc(xml)
|
67
116
|
end
|
68
117
|
|
118
|
+
# Perform an ad hoc scan of a subset of assets for a site by adding a specific runtime.
|
119
|
+
# Only assets from a single site should be submitted per request.
|
120
|
+
# Method is designed to take objects filtered from Site#assets.
|
121
|
+
#
|
122
|
+
# For example:
|
123
|
+
# site = Site.load(nsc, 5)
|
124
|
+
# nsc.scan_assets_with_schedule(5, site.assets.take(10), schedules)
|
125
|
+
#
|
126
|
+
# @param [Fixnum] site_id Site ID that the assets belong to.
|
127
|
+
# @param [Array[HostName|IPRange]] assets List of assets to scan.
|
128
|
+
# @param [Array[adhoc_schedules]] list of scheduled times at which to run
|
129
|
+
# @return [Status] whether the request was successful
|
130
|
+
#
|
131
|
+
def scan_assets_with_schedule(site_id, assets, schedules)
|
132
|
+
xml = make_xml('SiteDevicesScanRequest', 'site-id' => site_id)
|
133
|
+
hosts = REXML::Element.new('Hosts')
|
134
|
+
assets.each { |asset| _append_asset!(hosts, asset) }
|
135
|
+
xml.add_element(hosts)
|
136
|
+
scheds = REXML::Element.new('Schedules')
|
137
|
+
schedules.each { |sched| scheds.add_element(sched.as_xml) }
|
138
|
+
xml.add_element(scheds)
|
139
|
+
|
140
|
+
_scan_ad_hoc_with_schedules(xml)
|
141
|
+
end
|
142
|
+
|
143
|
+
# Perform an ad hoc scan of a subset of IP addresses for a site at a specific time.
|
144
|
+
# Only IPs from a single site can be submitted per request,
|
145
|
+
# and IP addresses must already be included in the site configuration.
|
146
|
+
# Method is designed for scanning when the targets are coming from an
|
147
|
+
# external source that does not have access to internal identfiers.
|
148
|
+
#
|
149
|
+
# For example:
|
150
|
+
# to_scan = ['192.168.2.1', '192.168.2.107']
|
151
|
+
# nsc.scan_ips(5, to_scan)
|
152
|
+
#
|
153
|
+
# @param [Fixnum] site_id Site ID that the assets belong to.
|
154
|
+
# @param [Array[String]] ip_addresses Array of IP addresses to scan.
|
155
|
+
# @return [Status] whether the request was successful
|
156
|
+
#
|
157
|
+
def scan_ips_with_schedule(site_id, ip_addresses, schedules)
|
158
|
+
xml = make_xml('SiteDevicesScanRequest', 'site-id' => site_id)
|
159
|
+
hosts = REXML::Element.new('Hosts')
|
160
|
+
ip_addresses.each do |ip|
|
161
|
+
xml.add_element('range', 'from' => ip)
|
162
|
+
end
|
163
|
+
xml.add_element(hosts)
|
164
|
+
scheds = REXML::Element.new('Schedules')
|
165
|
+
schedules.each { |sched| scheds.add_element(sched.as_xml) }
|
166
|
+
xml.add_element(scheds)
|
167
|
+
|
168
|
+
_scan_ad_hoc_with_schedules(xml)
|
169
|
+
end
|
170
|
+
|
69
171
|
# Perform an ad hoc scan of a subset of IP addresses for a site.
|
70
172
|
# Only IPs from a single site can be submitted per request,
|
71
173
|
# and IP addresses must already be included in the site configuration.
|
@@ -81,10 +183,10 @@ module Nexpose
|
|
81
183
|
# @return [Scan] Scan launch information.
|
82
184
|
#
|
83
185
|
def scan_ips(site_id, ip_addresses)
|
84
|
-
xml = make_xml('SiteDevicesScanRequest',
|
186
|
+
xml = make_xml('SiteDevicesScanRequest', 'site-id' => site_id)
|
85
187
|
hosts = REXML::Element.new('Hosts')
|
86
188
|
ip_addresses.each do |ip|
|
87
|
-
xml.add_element('range',
|
189
|
+
xml.add_element('range', 'from' => ip)
|
88
190
|
end
|
89
191
|
xml.add_element(hosts)
|
90
192
|
|
@@ -97,7 +199,7 @@ module Nexpose
|
|
97
199
|
# @return [Scan] Scan launch information.
|
98
200
|
#
|
99
201
|
def scan_site(site_id)
|
100
|
-
xml = make_xml('SiteScanRequest',
|
202
|
+
xml = make_xml('SiteScanRequest', 'site-id' => site_id)
|
101
203
|
response = execute(xml)
|
102
204
|
Scan.parse(response.res) if response.success
|
103
205
|
end
|
@@ -110,7 +212,7 @@ module Nexpose
|
|
110
212
|
#
|
111
213
|
def _append_asset!(xml, asset)
|
112
214
|
if asset.is_a? Nexpose::IPRange
|
113
|
-
xml.add_element('range',
|
215
|
+
xml.add_element('range', 'from' => asset.from, 'to' => asset.to)
|
114
216
|
else # Assume HostName
|
115
217
|
host = REXML::Element.new('host')
|
116
218
|
host.text = asset.host
|
@@ -129,6 +231,15 @@ module Nexpose
|
|
129
231
|
Scan.parse(r.res)
|
130
232
|
end
|
131
233
|
|
234
|
+
# Utility method for executing prepared XML for adhoc with schedules
|
235
|
+
#
|
236
|
+
# @param [REXML::Document] xml Prepared API call to execute.
|
237
|
+
#
|
238
|
+
def _scan_ad_hoc_with_schedules(xml)
|
239
|
+
r = execute(xml, '1.1', timeout: 60)
|
240
|
+
r.success
|
241
|
+
end
|
242
|
+
|
132
243
|
# Stop a running or paused scan.
|
133
244
|
#
|
134
245
|
# @param [Fixnum] scan_id ID of the scan to stop.
|
@@ -136,7 +247,7 @@ module Nexpose
|
|
136
247
|
# updated.
|
137
248
|
#
|
138
249
|
def stop_scan(scan_id, wait_sec = 0)
|
139
|
-
r = execute(make_xml('ScanStopRequest',
|
250
|
+
r = execute(make_xml('ScanStopRequest', 'scan-id' => scan_id))
|
140
251
|
if r.success
|
141
252
|
so_far = 0
|
142
253
|
while so_far < wait_sec
|
@@ -155,7 +266,7 @@ module Nexpose
|
|
155
266
|
# @return [String] Current status of the scan. See Nexpose::Scan::Status.
|
156
267
|
#
|
157
268
|
def scan_status(scan_id)
|
158
|
-
r = execute(make_xml('ScanStatusRequest',
|
269
|
+
r = execute(make_xml('ScanStatusRequest', 'scan-id' => scan_id))
|
159
270
|
r.success ? r.attributes['status'] : nil
|
160
271
|
end
|
161
272
|
|
@@ -164,7 +275,7 @@ module Nexpose
|
|
164
275
|
# @param [Fixnum] scan_id The scan ID.
|
165
276
|
#
|
166
277
|
def resume_scan(scan_id)
|
167
|
-
r = execute(make_xml('ScanResumeRequest',
|
278
|
+
r = execute(make_xml('ScanResumeRequest', 'scan-id' => scan_id), '1.1', timeout: 60)
|
168
279
|
r.success ? r.attributes['success'] == '1' : false
|
169
280
|
end
|
170
281
|
|
@@ -173,7 +284,7 @@ module Nexpose
|
|
173
284
|
# @param [Fixnum] scan_id The scan ID.
|
174
285
|
#
|
175
286
|
def pause_scan(scan_id)
|
176
|
-
r = execute(make_xml('ScanPauseRequest',
|
287
|
+
r = execute(make_xml('ScanPauseRequest', 'scan-id' => scan_id))
|
177
288
|
r.success ? r.attributes['success'] == '1' : false
|
178
289
|
end
|
179
290
|
|
@@ -218,7 +329,7 @@ module Nexpose
|
|
218
329
|
# @return [ScanSummary] ScanSummary object providing statistics for the scan.
|
219
330
|
#
|
220
331
|
def scan_statistics(scan_id)
|
221
|
-
r = execute(make_xml('ScanStatisticsRequest',
|
332
|
+
r = execute(make_xml('ScanStatisticsRequest', 'scan-id' => scan_id))
|
222
333
|
if r.success
|
223
334
|
ScanSummary.parse(r.res.elements['//ScanSummary'])
|
224
335
|
else
|
@@ -353,6 +464,7 @@ module Nexpose
|
|
353
464
|
def initialize(scan_id, site_id, engine_id, status, start_time, end_time)
|
354
465
|
@scan_id, @site_id, @engine_id, @status, @start_time, @end_time = scan_id, site_id, engine_id, status, start_time, end_time
|
355
466
|
end
|
467
|
+
|
356
468
|
def self.parse(xml)
|
357
469
|
# Start time can be empty in some error conditions.
|
358
470
|
start_time = nil
|
@@ -382,7 +494,6 @@ module Nexpose
|
|
382
494
|
# Object that represents a summary of a scan.
|
383
495
|
#
|
384
496
|
class ScanSummary < ScanData
|
385
|
-
|
386
497
|
# The reason the scan was stopped or failed, if applicable.
|
387
498
|
attr_reader :message
|
388
499
|
|
@@ -408,7 +519,7 @@ module Nexpose
|
|
408
519
|
tasks = Tasks.parse(xml.elements['tasks'])
|
409
520
|
nodes = Nodes.parse(xml.elements['nodes'])
|
410
521
|
vulns = Vulnerabilities.parse(xml.attributes['scan-id'], xml)
|
411
|
-
msg = xml.elements['message'] ?
|
522
|
+
msg = xml.elements['message'] ? xml.elements['message'].text : nil
|
412
523
|
|
413
524
|
# Start time can be empty in some error conditions.
|
414
525
|
start_time = nil
|
@@ -440,7 +551,6 @@ module Nexpose
|
|
440
551
|
# Value class to tracking task counts.
|
441
552
|
#
|
442
553
|
class Tasks
|
443
|
-
|
444
554
|
attr_reader :pending, :active, :completed
|
445
555
|
|
446
556
|
def initialize(pending, active, completed)
|
@@ -463,7 +573,6 @@ module Nexpose
|
|
463
573
|
# Value class for tracking node counts.
|
464
574
|
#
|
465
575
|
class Nodes
|
466
|
-
|
467
576
|
attr_reader :live, :dead, :filtered, :unresolved, :other
|
468
577
|
|
469
578
|
def initialize(live, dead, filtered, unresolved, other)
|
@@ -488,7 +597,6 @@ module Nexpose
|
|
488
597
|
# Value class for tracking vulnerability counts.
|
489
598
|
#
|
490
599
|
class Vulnerabilities
|
491
|
-
|
492
600
|
attr_reader :vuln_exploit, :vuln_version, :vuln_potential,
|
493
601
|
:not_vuln_exploit, :not_vuln_version,
|
494
602
|
:error, :disabled, :other
|
@@ -497,11 +605,11 @@ module Nexpose
|
|
497
605
|
not_vuln_exploit, not_vuln_version,
|
498
606
|
error, disabled, other)
|
499
607
|
@vuln_exploit, @vuln_version, @vuln_potential,
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
608
|
+
@not_vuln_exploit, @not_vuln_version,
|
609
|
+
@error, @disabled, @other =
|
610
|
+
vuln_exploit, vuln_version, vuln_potential,
|
611
|
+
not_vuln_exploit, not_vuln_version,
|
612
|
+
error, disabled, other
|
505
613
|
end
|
506
614
|
|
507
615
|
# Parse REXML to Vulnerabilities object.
|