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
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.
|