nexpose 0.8.4 → 0.8.5
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/lib/nexpose.rb +8 -0
- data/lib/nexpose/discovery.rb +43 -5
- data/lib/nexpose/scan.rb +20 -43
- data/lib/nexpose/site.rb +63 -19
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a14fda65d152d074a2adba56c7180417b04c9569
|
4
|
+
data.tar.gz: 6e349f9013e13f8b2972ada209543dee513d612a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: aaf259cadc6172ca32f9a07d6a15240dab9c0baf0f565ffe070c16472c7d3c0768ae86850cc4850eaf9221bf292d00f9c5619566fa62f447e83289e930ef0b04
|
7
|
+
data.tar.gz: a7b6825ce9cd0898acdd6784e106c7a3823c88ec1bf227ccd13de18d0355132aea1532d14cf3b458af9f412bb7561a9f900d02715a745edaab7b9f9f6e9724f0
|
data/lib/nexpose.rb
CHANGED
@@ -103,3 +103,11 @@ module Nexpose
|
|
103
103
|
puts 'response: ' + object.response_xml.to_s
|
104
104
|
end
|
105
105
|
end
|
106
|
+
|
107
|
+
# Monkey patch from ActiveSupport which rex 2.0.3 incorrectly replies upon.
|
108
|
+
# This enables the multipart MIME handling in Connection#import_scan
|
109
|
+
class String
|
110
|
+
def blank?
|
111
|
+
self !~ /\S/
|
112
|
+
end
|
113
|
+
end
|
data/lib/nexpose/discovery.rb
CHANGED
@@ -35,6 +35,11 @@ module Nexpose
|
|
35
35
|
HTTPS = 'HTTPS'
|
36
36
|
end
|
37
37
|
|
38
|
+
module Type
|
39
|
+
V_SPHERE = 'VSPHERE'
|
40
|
+
AWS = 'AWS'
|
41
|
+
end
|
42
|
+
|
38
43
|
# A unique identifier for this connection.
|
39
44
|
attr_accessor :id
|
40
45
|
|
@@ -42,6 +47,7 @@ module Nexpose
|
|
42
47
|
attr_accessor :name
|
43
48
|
|
44
49
|
# The IP address or fully qualified domain name of the server.
|
50
|
+
# For AWS connections, this is the address of the regional server.
|
45
51
|
attr_accessor :address
|
46
52
|
|
47
53
|
# A user name that can be used to log into the server.
|
@@ -56,6 +62,12 @@ module Nexpose
|
|
56
62
|
# The port used for connecting to the server. A valid port from 1 to 65535.
|
57
63
|
attr_accessor :port
|
58
64
|
|
65
|
+
# The type of discovery connection.
|
66
|
+
attr_accessor :type
|
67
|
+
|
68
|
+
# Whether the console and scan engine are inside the AWS network.
|
69
|
+
attr_accessor :inside_network
|
70
|
+
|
59
71
|
# Whether or not the connection is active.
|
60
72
|
# Discovery is only possible when the connection is active.
|
61
73
|
attr_accessor :status
|
@@ -73,6 +85,7 @@ module Nexpose
|
|
73
85
|
@id = -1
|
74
86
|
@port = 443
|
75
87
|
@protocol = Protocol::HTTPS
|
88
|
+
@type = Type::V_SPHERE
|
76
89
|
end
|
77
90
|
|
78
91
|
# Save this discovery connection to a Nexpose console.
|
@@ -130,13 +143,35 @@ module Nexpose
|
|
130
143
|
end
|
131
144
|
|
132
145
|
def as_xml
|
146
|
+
# TODO
|
147
|
+
# <DiscoveryConfig id="-1" name="aws" port="443" protocol="HTTPS" username="asdf" type="AWS" usePrivateIPAddress="0" engineid="2" region="ec2.us-west-1.amazonaws.com" password="asdf"> </DiscoveryConfig>
|
148
|
+
# <DiscoveryConfig id="-1" name="external AWS" port="443" protocol="HTTPS" username="acess-key-id" type="AWS" usePrivateIPAddress="0" engineid="2" region="ec2.sa-east-1.amazonaws.com" password="secret Access Key">
|
149
|
+
# <DiscoveryConfig id="-1" name="internal-aws" port="443" protocol="HTTPS" username=" " type="AWS" usePrivateIPAddress="1" engineid="2" region="ec2.ap-northeast-1.amazonaws.com" password="Not required">
|
150
|
+
# <DiscoveryConfig id="-1" name="V the Final Battle" fqdn="vcenter001.osdc.lax.rapid7.com" port="443" protocol="HTTPS" username="TOR\lmnexpose" type="VSPHERE" usePrivateIPAddress="0" engineid="2" region="ec2.us-east-1.amazonaws.com" password="22yQCA0P8nSvfrE">
|
151
|
+
# <DiscoveryConfig id="-1" name="duplicate vSphere" fqdn="vcenter001.osdc.lax.rapid7.com" port="443" protocol="HTTPS" username="blah" type="VSPHERE" usePrivateIPAddress="0" engineid="2" region="ec2.us-east-1.amazonaws.com" password="blah">
|
152
|
+
# <DiscoveryConfig id="10" name ="vCenter" fqdn ="vcenter001.osdc.lax.rapid7.com" port ="443" protocol ="HTTPS" username ="TOR\lmnexpose" type="VSPHERE" usePrivateIPAddress="0"> </DiscoveryConfig>
|
133
153
|
xml = REXML::Element.new('DiscoveryConnection')
|
134
|
-
xml.add_attributes({ '
|
135
|
-
'
|
154
|
+
xml.add_attributes({ 'id' => @id,
|
155
|
+
'name' => @name,
|
136
156
|
'port' => @port,
|
137
|
-
'protocol' => @protocol
|
138
|
-
|
139
|
-
|
157
|
+
'protocol' => @protocol })
|
158
|
+
case @type
|
159
|
+
when Type::AWS
|
160
|
+
xml.add_attributes({ 'region' => @address })
|
161
|
+
if @inside_network
|
162
|
+
xml.add_attributes({ 'usePrivateIPAddress' => 1,
|
163
|
+
'username' => ' ',
|
164
|
+
'password' => 'Not required' })
|
165
|
+
else
|
166
|
+
xml.add_attributes({ 'usePrivateIPAddress' => 0,
|
167
|
+
'username' => @user,
|
168
|
+
'password' => @password })
|
169
|
+
end
|
170
|
+
when Type::V_SPHERE
|
171
|
+
xml.add_attributes({ 'fqdn' => @address,
|
172
|
+
'username' => @user,
|
173
|
+
'password' => @password })
|
174
|
+
end
|
140
175
|
xml
|
141
176
|
end
|
142
177
|
|
@@ -145,6 +180,9 @@ module Nexpose
|
|
145
180
|
end
|
146
181
|
|
147
182
|
def self.parse(xml)
|
183
|
+
# <DiscoveryConfig id="10" name ="vCenter" fqdn ="vcenter001.osdc.lax.rapid7.com" port ="443" protocol ="HTTPS" username ="TOR\lmnexpose" type="VSPHERE" usePrivateIPAddress="0"> </DiscoveryConfig>
|
184
|
+
# <DiscoveryConfig id="34" name ="external AWS" region ="ec2.sa-east-1.amazonaws.com" port ="443" protocol ="HTTPS" username ="acess-key-id" type="AWS" usePrivateIPAddress="0">
|
185
|
+
# <DiscoveryConfig id="32" name ="my-aws" region ="ec2.ap-northeast-1.amazonaws.com" port ="443" protocol ="HTTPS" username =" " type="AWS" usePrivateIPAddress="1">
|
148
186
|
conn = new(xml.attributes['name'],
|
149
187
|
xml.attributes['address'],
|
150
188
|
xml.attributes['user-name'])
|
data/lib/nexpose/scan.rb
CHANGED
@@ -256,51 +256,28 @@ module Nexpose
|
|
256
256
|
# @return [String] An empty string on success.
|
257
257
|
#
|
258
258
|
def import_scan(site_id, zip_file)
|
259
|
+
data = Rex::MIME::Message.new
|
260
|
+
data.add_part(site_id.to_s, nil, nil, 'form-data; name="siteid"')
|
261
|
+
data.add_part(self.session_id, nil, nil, 'form-data; name="nexposeCCSessionID"')
|
262
|
+
scan = File.new(zip_file, 'rb')
|
263
|
+
data.add_part(scan.read, 'application/zip', 'binary',
|
264
|
+
"form-data; name=\"scan\"; filename=\"#{zip_file}\"")
|
259
265
|
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
# # pending discovery of what to do.
|
264
|
-
|
265
|
-
# data = Rex::MIME::Message.new
|
266
|
-
# data.add_part(site_id.to_s, nil, nil, 'form-data; name="siteid"')
|
267
|
-
# data.add_part(self.session_id, nil, nil, 'form-data; name="nexposeCCSessionID"')
|
268
|
-
|
269
|
-
# scan = File.new(zip_file, 'rb')
|
270
|
-
# data.add_part(scan.read, 'application/zip', nil,
|
271
|
-
# "form-data; name=\"scan\"; filename=\"#{zip_file}\"")
|
272
|
-
|
273
|
-
# post = Net::HTTP::Post.new('/data/scan/import')
|
274
|
-
# ## rex 2.0.3 has a bug that requires this monkey-patch for Message#to_s
|
275
|
-
# # class String
|
276
|
-
# # def blank?
|
277
|
-
# # self !~ /\S/
|
278
|
-
# # end
|
279
|
-
# # end
|
280
|
-
# post.body = data.to_s
|
281
|
-
# post.set_content_type("multipart/form-data; boundary=#{data.bound}")
|
282
|
-
# AJAX._headers(nsc, post)
|
283
|
-
|
284
|
-
# http = AJAX._https(nsc)
|
285
|
-
# http.request(post)
|
266
|
+
post = Net::HTTP::Post.new('/data/scan/import')
|
267
|
+
post.body = data.to_s
|
268
|
+
post.set_content_type('multipart/form-data', boundary: data.bound)
|
286
269
|
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
begin
|
299
|
-
request.execute
|
300
|
-
rescue RestClient::Forbidden => fourOhThree
|
301
|
-
raise Nexpose::PermissionError.new(fourOhThree)
|
302
|
-
rescue RestClient::InternalServerError => e
|
303
|
-
raise Nexpose::APIError.new(request, e)
|
270
|
+
# Avoiding AJAX#request, because the data can cause binary dump on error.
|
271
|
+
http = AJAX._https(self)
|
272
|
+
AJAX._headers(self, post)
|
273
|
+
response = http.request(post)
|
274
|
+
case response
|
275
|
+
when Net::HTTPOK
|
276
|
+
response.body
|
277
|
+
when Net::HTTPUnauthorized
|
278
|
+
raise Nexpose::PermissionError.new(response)
|
279
|
+
else
|
280
|
+
raise Nexpose::APIError.new(post, response.body)
|
304
281
|
end
|
305
282
|
end
|
306
283
|
|
data/lib/nexpose/site.rb
CHANGED
@@ -174,6 +174,13 @@ module Nexpose
|
|
174
174
|
@assets << HostName.new(hostname)
|
175
175
|
end
|
176
176
|
|
177
|
+
# Remove an asset to this site by host name.
|
178
|
+
#
|
179
|
+
# @param [String] hostname FQDN or DNS-resolvable host name of an asset.
|
180
|
+
def remove_host(hostname)
|
181
|
+
@assets = assets.reject { |asset| asset == HostName.new(hostname) }
|
182
|
+
end
|
183
|
+
|
177
184
|
# Adds an asset to this site by IP address.
|
178
185
|
#
|
179
186
|
# @param [String] ip IP address of an asset.
|
@@ -181,6 +188,13 @@ module Nexpose
|
|
181
188
|
@assets << IPRange.new(ip)
|
182
189
|
end
|
183
190
|
|
191
|
+
# Remove an asset to this site by IP address.
|
192
|
+
#
|
193
|
+
# @param [String] ip IP address of an asset.
|
194
|
+
def remove_ip(ip)
|
195
|
+
@assets = assets.reject { |asset| asset == IPRange.new(ip) }
|
196
|
+
end
|
197
|
+
|
184
198
|
# Adds assets to this site by IP address range.
|
185
199
|
#
|
186
200
|
# @param [String] from Beginning IP address of a range.
|
@@ -189,6 +203,14 @@ module Nexpose
|
|
189
203
|
@assets << IPRange.new(from, to)
|
190
204
|
end
|
191
205
|
|
206
|
+
# Remove assets to this site by IP address range.
|
207
|
+
#
|
208
|
+
# @param [String] from Beginning IP address of a range.
|
209
|
+
# @param [String] to Ending IP address of a range.
|
210
|
+
def remove_ip_range(from, to)
|
211
|
+
@assets = assets.reject { |asset| asset == IPRange.new(from, to) }
|
212
|
+
end
|
213
|
+
|
192
214
|
# Adds an asset to this site, resolving whether an IP or hostname is
|
193
215
|
# provided.
|
194
216
|
#
|
@@ -208,6 +230,25 @@ module Nexpose
|
|
208
230
|
end
|
209
231
|
end
|
210
232
|
|
233
|
+
# Remove an asset to this site, resolving whether an IP or hostname is
|
234
|
+
# provided.
|
235
|
+
#
|
236
|
+
# @param [String] asset Identifier of an asset, either IP or host name.
|
237
|
+
#
|
238
|
+
def remove_asset(asset)
|
239
|
+
begin
|
240
|
+
# If the asset registers as a valid IP, store as IP.
|
241
|
+
ip = IPAddr.new(asset)
|
242
|
+
remove_ip(asset)
|
243
|
+
rescue ArgumentError => e
|
244
|
+
if e.message == 'invalid address'
|
245
|
+
remove_host(asset)
|
246
|
+
else
|
247
|
+
raise "Unable to parse asset: '#{asset}'. #{e.message}"
|
248
|
+
end
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
211
252
|
# Load an existing configuration from a Nexpose instance.
|
212
253
|
#
|
213
254
|
# @param [Connection] connection Connection to console where site exists.
|
@@ -253,8 +294,8 @@ module Nexpose
|
|
253
294
|
|
254
295
|
# Have to retrieve and attach shared creds, or saving will fail.
|
255
296
|
xml = _append_shared_creds_to_xml(connection, as_xml)
|
256
|
-
response = AJAX.post(connection, '/
|
257
|
-
saved = REXML::XPath.first(REXML::Document.new(response), '
|
297
|
+
response = AJAX.post(connection, '/data/site/config', xml)
|
298
|
+
saved = REXML::XPath.first(REXML::Document.new(response), 'ajaxResponse')
|
258
299
|
raise APIError.new(response, 'Failed to save dynamic site.') if saved.nil? || saved.attributes['success'].to_i != 1
|
259
300
|
|
260
301
|
save_dynamic_criteria(connection) unless new_site
|
@@ -297,8 +338,22 @@ module Nexpose
|
|
297
338
|
# @return [Fixnum] Site ID.
|
298
339
|
#
|
299
340
|
def save_dynamic_criteria(nsc)
|
300
|
-
|
301
|
-
|
341
|
+
# Several parameters are passed through the URI
|
342
|
+
params = { 'configID' => @discovery_connection_id,
|
343
|
+
'entityid' => @id > 0 ? @id : false,
|
344
|
+
'mode' => @id > 0 ? 'edit' : false }
|
345
|
+
uri = AJAX.parameterize_uri('/data/site/saveSite', params)
|
346
|
+
|
347
|
+
# JSON body of POST request contains details.
|
348
|
+
details = { 'dynamic' => true,
|
349
|
+
'name' => @name,
|
350
|
+
'tag' => @description.nil? ? '' : @description,
|
351
|
+
'riskFactor' => @risk_factor,
|
352
|
+
# 'vCenter' => @discovery_connection_id,
|
353
|
+
'searchCriteria' => @criteria.nil? ? { 'operator' => 'AND' } : @criteria.to_map }
|
354
|
+
json = JSON.generate(details)
|
355
|
+
|
356
|
+
response = AJAX.post(nsc, uri, json, AJAX::CONTENT_TYPE::JSON)
|
302
357
|
json = JSON.parse(response)
|
303
358
|
if json['response'] =~ /success/
|
304
359
|
if @id < 1
|
@@ -335,7 +390,9 @@ module Nexpose
|
|
335
390
|
xml.attributes['name'] = @name
|
336
391
|
xml.attributes['description'] = @description
|
337
392
|
xml.attributes['riskfactor'] = @risk_factor
|
338
|
-
xml.attributes['isDynamic']
|
393
|
+
xml.attributes['isDynamic'] = '1' if dynamic?
|
394
|
+
# TODO This should be set to 'Amazon Web Services' for AWS.
|
395
|
+
xml.attributes['dynamicConfigType'] = 'vSphere' if dynamic?
|
339
396
|
|
340
397
|
if @description && !@description.empty?
|
341
398
|
elem = REXML::Element.new('Description')
|
@@ -394,19 +451,6 @@ module Nexpose
|
|
394
451
|
as_xml.to_s
|
395
452
|
end
|
396
453
|
|
397
|
-
def to_dynamic_map
|
398
|
-
details = { 'dynamic' => true,
|
399
|
-
'name' => @name,
|
400
|
-
'tag' => @description.nil? ? '' : @description,
|
401
|
-
'riskFactor' => @risk_factor,
|
402
|
-
# 'vCenter' => @discovery_connection_id,
|
403
|
-
'searchCriteria' => @criteria.nil? ? { 'operator' => 'AND' } : @criteria.to_map }
|
404
|
-
params = { 'configID' => @discovery_connection_id,
|
405
|
-
'entityid' => @id > 0 ? @id : false,
|
406
|
-
'mode' => @id > 0 ? 'edit' : false,
|
407
|
-
'entityDetails' => details }
|
408
|
-
end
|
409
|
-
|
410
454
|
# Parse a response from a Nexpose console into a valid Site object.
|
411
455
|
#
|
412
456
|
# @param [REXML::Document] rexml XML document to parse.
|
@@ -475,7 +519,7 @@ module Nexpose
|
|
475
519
|
end
|
476
520
|
|
477
521
|
def _append_shared_creds_to_xml(connection, xml)
|
478
|
-
xml_w_creds = AJAX.get(connection, "/
|
522
|
+
xml_w_creds = AJAX.get(connection, "/data/site/config?siteid=#{@id}")
|
479
523
|
cred_xml = REXML::XPath.first(REXML::Document.new(xml_w_creds), 'Site/Credentials')
|
480
524
|
unless cred_xml.nil?
|
481
525
|
creds = REXML::XPath.first(xml, 'Credentials')
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: nexpose
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.8.
|
4
|
+
version: 0.8.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- HD Moore
|
@@ -11,7 +11,7 @@ authors:
|
|
11
11
|
autorequire:
|
12
12
|
bindir: bin
|
13
13
|
cert_chain: []
|
14
|
-
date: 2014-10-
|
14
|
+
date: 2014-10-09 00:00:00.000000000 Z
|
15
15
|
dependencies:
|
16
16
|
- !ruby/object:Gem::Dependency
|
17
17
|
name: rex
|