nexpose 0.0.9 → 0.0.91
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.
- data/lib/nexpose.rb +13 -3236
- data/lib/nexpose/api_request.rb +144 -0
- data/lib/nexpose/connection.rb +106 -0
- data/lib/nexpose/creds.rb +189 -0
- data/lib/nexpose/error.rb +21 -0
- data/lib/nexpose/misc.rb +122 -0
- data/lib/nexpose/report.rb +570 -0
- data/lib/nexpose/scan.rb +280 -0
- data/lib/nexpose/scan_engine.rb +147 -0
- data/lib/nexpose/silo.rb +347 -0
- data/lib/nexpose/site.rb +877 -0
- data/lib/nexpose/ticket.rb +108 -0
- data/lib/nexpose/util.rb +35 -0
- data/lib/nexpose/vuln.rb +520 -0
- data/nexpose.gemspec +3 -3
- metadata +22 -11
@@ -0,0 +1,144 @@
|
|
1
|
+
module Nexpose
|
2
|
+
class APIRequest
|
3
|
+
include XMLUtils
|
4
|
+
|
5
|
+
attr_reader :http
|
6
|
+
attr_reader :uri
|
7
|
+
attr_reader :headers
|
8
|
+
attr_reader :retry_count
|
9
|
+
attr_reader :time_out
|
10
|
+
attr_reader :pause
|
11
|
+
|
12
|
+
attr_reader :req
|
13
|
+
attr_reader :res
|
14
|
+
attr_reader :sid
|
15
|
+
attr_reader :success
|
16
|
+
|
17
|
+
attr_reader :error
|
18
|
+
attr_reader :trace
|
19
|
+
|
20
|
+
attr_reader :raw_response
|
21
|
+
attr_reader :raw_response_data
|
22
|
+
|
23
|
+
#
|
24
|
+
#
|
25
|
+
#
|
26
|
+
def initialize(req, url, api_version='1.1')
|
27
|
+
@url = url
|
28
|
+
@req = req
|
29
|
+
@api_version = api_version
|
30
|
+
@url = @url.sub('API_VERSION', @api_version)
|
31
|
+
prepare_http_client
|
32
|
+
end
|
33
|
+
|
34
|
+
#
|
35
|
+
#
|
36
|
+
#
|
37
|
+
def prepare_http_client
|
38
|
+
@retry_count = 0
|
39
|
+
@retry_count_max = 10
|
40
|
+
@time_out = 30
|
41
|
+
@pause = 2
|
42
|
+
@uri = URI.parse(@url)
|
43
|
+
@http = Net::HTTP.new(@uri.host, @uri.port)
|
44
|
+
@http.use_ssl = true
|
45
|
+
#
|
46
|
+
# XXX: This is obviously a security issue, however, we handle this at the client level by forcing
|
47
|
+
# a confirmation when the nexpose host is not localhost. In a perfect world, we would present
|
48
|
+
# the server signature before accepting it, but this requires either a direct callback inside
|
49
|
+
# of this module back to whatever UI, or opens a race condition between accept and attempt.
|
50
|
+
#
|
51
|
+
@http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
52
|
+
@headers = {'Content-Type' => 'text/xml'}
|
53
|
+
@success = false
|
54
|
+
end
|
55
|
+
|
56
|
+
#
|
57
|
+
#
|
58
|
+
#
|
59
|
+
def execute
|
60
|
+
@conn_tries = 0
|
61
|
+
|
62
|
+
begin
|
63
|
+
prepare_http_client
|
64
|
+
@raw_response = @http.post(@uri.path, @req, @headers)
|
65
|
+
@raw_response_data = @raw_response.read_body
|
66
|
+
@res = parse_xml(@raw_response_data)
|
67
|
+
|
68
|
+
if (not @res.root)
|
69
|
+
@error = "NeXpose service returned invalid XML"
|
70
|
+
return @sid
|
71
|
+
end
|
72
|
+
|
73
|
+
@sid = attributes['session-id']
|
74
|
+
|
75
|
+
if (attributes['success'] and attributes['success'].to_i == 1)
|
76
|
+
@success = true
|
77
|
+
elsif @api_version =~ /1.2/ and @res and (@res.get_elements '//Exception').count < 1
|
78
|
+
@success = true
|
79
|
+
else
|
80
|
+
@success = false
|
81
|
+
@res.elements.each('//Failure/Exception') do |s|
|
82
|
+
s.elements.each('message') do |m|
|
83
|
+
@error = m.text
|
84
|
+
end
|
85
|
+
s.elements.each('stacktrace') do |m|
|
86
|
+
@trace = m.text
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
# This is a hack to handle corner cases where a heavily loaded NeXpose instance
|
91
|
+
# drops our HTTP connection before processing. We try 5 times to establish a
|
92
|
+
# connection in these situations. The actual exception occurs in the Ruby
|
93
|
+
# http library, which is why we use such generic error classes.
|
94
|
+
rescue ::ArgumentError, ::NoMethodError
|
95
|
+
if @conn_tries < 5
|
96
|
+
@conn_tries += 1
|
97
|
+
retry
|
98
|
+
end
|
99
|
+
rescue ::Timeout::Error
|
100
|
+
if @conn_tries < 5
|
101
|
+
@conn_tries += 1
|
102
|
+
retry
|
103
|
+
end
|
104
|
+
@error = "NeXpose host did not respond"
|
105
|
+
rescue ::Errno::EHOSTUNREACH, ::Errno::ENETDOWN, ::Errno::ENETUNREACH, ::Errno::ENETRESET, ::Errno::EHOSTDOWN, ::Errno::EACCES, ::Errno::EINVAL, ::Errno::EADDRNOTAVAIL
|
106
|
+
@error = "NeXpose host is unreachable"
|
107
|
+
# Handle console-level interrupts
|
108
|
+
rescue ::Interrupt
|
109
|
+
@error = "received a user interrupt"
|
110
|
+
rescue ::Errno::ECONNRESET, ::Errno::ECONNREFUSED, ::Errno::ENOTCONN, ::Errno::ECONNABORTED
|
111
|
+
@error = "NeXpose service is not available"
|
112
|
+
rescue ::REXML::ParseException
|
113
|
+
@error = "NeXpose has not been properly licensed"
|
114
|
+
end
|
115
|
+
|
116
|
+
if !(@success or @error)
|
117
|
+
@error = "NeXpose service returned an unrecognized response: #{@raw_response_data.inspect}"
|
118
|
+
end
|
119
|
+
|
120
|
+
@sid
|
121
|
+
end
|
122
|
+
|
123
|
+
#
|
124
|
+
#
|
125
|
+
#
|
126
|
+
def attributes(*args)
|
127
|
+
return if not @res.root
|
128
|
+
@res.root.attributes(*args)
|
129
|
+
end
|
130
|
+
|
131
|
+
#
|
132
|
+
#
|
133
|
+
#
|
134
|
+
def self.execute(url, req, api_version='1.1')
|
135
|
+
obj = self.new(req, url, api_version)
|
136
|
+
obj.execute
|
137
|
+
if (not obj.success)
|
138
|
+
raise APIError.new(obj, "Action failed: #{obj.error}")
|
139
|
+
end
|
140
|
+
obj
|
141
|
+
end
|
142
|
+
|
143
|
+
end
|
144
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
module Nexpose
|
2
|
+
|
3
|
+
# === Description
|
4
|
+
# Object that represents a connection to a NeXpose Security Console.
|
5
|
+
#
|
6
|
+
# === Examples
|
7
|
+
# # Create a new Nexpose Connection on the default port
|
8
|
+
# nsc = Connection.new("10.1.40.10","nxadmin","password")
|
9
|
+
#
|
10
|
+
# # Login to NSC and Establish a Session ID
|
11
|
+
# nsc.login()
|
12
|
+
#
|
13
|
+
# # Check Session ID
|
14
|
+
# if (nsc.session_id)
|
15
|
+
# puts "Login Successful"
|
16
|
+
# else
|
17
|
+
# puts "Login Failure"
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# # //Logout
|
21
|
+
# logout_success = nsc.logout()
|
22
|
+
# if (! logout_success)
|
23
|
+
# puts "Logout Failure" + "<p>" + nsc.error_msg.to_s
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
class Connection
|
27
|
+
include XMLUtils
|
28
|
+
include NexposeAPI
|
29
|
+
|
30
|
+
# true if an error condition exists; false otherwise
|
31
|
+
attr_reader :error
|
32
|
+
# Error message string
|
33
|
+
attr_reader :error_msg
|
34
|
+
# The last XML request sent by this object
|
35
|
+
attr_reader :request_xml
|
36
|
+
# The last XML response received by this object
|
37
|
+
attr_reader :response_xml
|
38
|
+
# Session ID of this connection
|
39
|
+
attr_reader :session_id
|
40
|
+
# The hostname or IP Address of the NSC
|
41
|
+
attr_reader :host
|
42
|
+
# The port of the NSC (default is 3780)
|
43
|
+
attr_reader :port
|
44
|
+
# The username used to login to the NSC
|
45
|
+
attr_reader :username
|
46
|
+
# The password used to login to the NSC
|
47
|
+
attr_reader :password
|
48
|
+
# The URL for communication
|
49
|
+
attr_reader :url
|
50
|
+
|
51
|
+
# Constructor for Connection
|
52
|
+
def initialize(ip, user, pass, port = 3780, silo_id = nil)
|
53
|
+
@host = ip
|
54
|
+
@port = port
|
55
|
+
@username = user
|
56
|
+
@password = pass
|
57
|
+
@silo_id = silo_id
|
58
|
+
@session_id = nil
|
59
|
+
@error = false
|
60
|
+
@url = "https://#{@host}:#{@port}/api/API_VERSION/xml"
|
61
|
+
end
|
62
|
+
|
63
|
+
# Establish a new connection and Session ID
|
64
|
+
def login
|
65
|
+
begin
|
66
|
+
login_hash = {'sync-id' => 0, 'password' => @password, 'user-id' => @username}
|
67
|
+
unless @silo_id.nil?
|
68
|
+
login_hash['silo-id'] = @silo_id
|
69
|
+
end
|
70
|
+
r = execute(make_xml('LoginRequest', login_hash))
|
71
|
+
rescue APIError
|
72
|
+
raise AuthenticationFailed.new(r)
|
73
|
+
end
|
74
|
+
if (r.success)
|
75
|
+
@session_id = r.sid
|
76
|
+
true
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Logout of the current connection
|
81
|
+
def logout
|
82
|
+
r = execute(make_xml('LogoutRequest', {'sync-id' => 0}))
|
83
|
+
if (r.success)
|
84
|
+
return true
|
85
|
+
end
|
86
|
+
raise APIError.new(r, 'Logout failed')
|
87
|
+
end
|
88
|
+
|
89
|
+
# Execute an API request
|
90
|
+
def execute(xml, version = '1.1')
|
91
|
+
@api_version = version
|
92
|
+
APIRequest.execute(@url, xml.to_s, @api_version)
|
93
|
+
end
|
94
|
+
|
95
|
+
# Download a specific URL
|
96
|
+
def download(url)
|
97
|
+
uri = URI.parse(url)
|
98
|
+
http = Net::HTTP.new(@host, @port)
|
99
|
+
http.use_ssl = true
|
100
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE # XXX: security issue
|
101
|
+
headers = {'Cookie' => "nexposeCCSessionID=#{@session_id}"}
|
102
|
+
resp, data = http.get(uri.path, headers)
|
103
|
+
data
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,189 @@
|
|
1
|
+
module Nexpose
|
2
|
+
include Sanitize
|
3
|
+
|
4
|
+
# === Description
|
5
|
+
# Object that represents administrative credentials to be used during a scan. When retrived from an existing site configuration the credentials will be returned as a security blob and can only be passed back as is during a Site Save operation. This object can only be used to create a new set of credentials.
|
6
|
+
#
|
7
|
+
class AdminCredentials
|
8
|
+
# Security blob for an existing set of credentials
|
9
|
+
attr_reader :securityblob
|
10
|
+
# Designates if this object contains user defined credentials or a security blob
|
11
|
+
attr_reader :isblob
|
12
|
+
# The service for these credentials. Can be All.
|
13
|
+
attr_reader :service
|
14
|
+
# The host for these credentials. Can be Any.
|
15
|
+
attr_reader :host
|
16
|
+
# The port on which to use these credentials.
|
17
|
+
attr_reader :port
|
18
|
+
# The user id or username
|
19
|
+
attr_reader :userid
|
20
|
+
# The password
|
21
|
+
attr_reader :password
|
22
|
+
# The realm for these credentials
|
23
|
+
attr_reader :realm
|
24
|
+
# When using httpheaders, this represents the set of headers to pass
|
25
|
+
# with the authentication request.
|
26
|
+
attr_reader :headers
|
27
|
+
|
28
|
+
def initialize(isblob = false)
|
29
|
+
@isblob = isblob
|
30
|
+
end
|
31
|
+
|
32
|
+
# Sets the credentials information for this object.
|
33
|
+
def setCredentials(service, host, port, userid, password, realm)
|
34
|
+
@isblob = false
|
35
|
+
@securityblob = nil
|
36
|
+
@service = service
|
37
|
+
@host = host
|
38
|
+
@port = port
|
39
|
+
@userid = userid
|
40
|
+
@password = password
|
41
|
+
@realm = realm
|
42
|
+
end
|
43
|
+
|
44
|
+
# TODO: add description
|
45
|
+
def setService(service)
|
46
|
+
@service = service
|
47
|
+
end
|
48
|
+
|
49
|
+
def setHost(host)
|
50
|
+
@host = host
|
51
|
+
end
|
52
|
+
|
53
|
+
# TODO: add description
|
54
|
+
def setBlob(securityblob)
|
55
|
+
@isblob = true
|
56
|
+
@securityblob = securityblob
|
57
|
+
end
|
58
|
+
|
59
|
+
# Add Headers to credentials for httpheaders.
|
60
|
+
def setHeaders(headers)
|
61
|
+
@headers = headers
|
62
|
+
end
|
63
|
+
|
64
|
+
def to_xml
|
65
|
+
xml = ''
|
66
|
+
xml << '<adminCredentials'
|
67
|
+
xml << %Q{ service="#{replace_entities(service)}"} if (service)
|
68
|
+
xml << %Q{ userid="#{replace_entities(userid)}"} if (userid)
|
69
|
+
xml << %Q{ password="#{replace_entities(password)}"} if (password)
|
70
|
+
xml << %Q{ realm="#{replace_entities(realm)}"} if (realm)
|
71
|
+
xml << %Q{ host="#{replace_entities(host)}"} if (host)
|
72
|
+
xml << %Q{ port="#{replace_entities(port)}"} if (port)
|
73
|
+
xml << '>'
|
74
|
+
xml << replace_entities(securityblob) if (isblob)
|
75
|
+
xml << @headers.to_xml()
|
76
|
+
xml << '</adminCredentials>'
|
77
|
+
|
78
|
+
xml
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Object that represents Header name-value pairs, associated with Web Session Authentication.
|
83
|
+
class Header
|
84
|
+
# Name, one per Header
|
85
|
+
attr_reader :name
|
86
|
+
# Value, one per Header
|
87
|
+
attr_reader :value
|
88
|
+
|
89
|
+
# Construct with name value pair
|
90
|
+
def initialize(name, value)
|
91
|
+
@name = name
|
92
|
+
@value = value
|
93
|
+
end
|
94
|
+
|
95
|
+
def to_xml
|
96
|
+
xml = ''
|
97
|
+
xml << '<Header'
|
98
|
+
xml << %Q{ name="#{replace_entities(name)}"} if (name)
|
99
|
+
xml << %Q{ value="#{replace_entities(value)}"} if (value)
|
100
|
+
xml << '/>'
|
101
|
+
xml
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# Object that represents Headers, associated with Web Session Authentication.
|
106
|
+
class Headers
|
107
|
+
# A regular expression used to match against the response to identify authentication failures.
|
108
|
+
attr_reader :soft403
|
109
|
+
# Base URL of the application for which the form authentication applies.
|
110
|
+
attr_reader :webapproot
|
111
|
+
# When using httpheaders, this represents the set of headers to pass with the authentication request.
|
112
|
+
attr_reader :headers
|
113
|
+
|
114
|
+
def initialize(webapproot, soft403)
|
115
|
+
@headers = []
|
116
|
+
@webapproot = webapproot
|
117
|
+
@soft403 = soft403
|
118
|
+
end
|
119
|
+
|
120
|
+
def addHeader(header)
|
121
|
+
@headers.push(header)
|
122
|
+
end
|
123
|
+
|
124
|
+
|
125
|
+
def to_xml
|
126
|
+
xml = ''
|
127
|
+
xml << '<Headers'
|
128
|
+
xml << %Q{ soft403="#{replace_entities(soft403)}"} if (soft403)
|
129
|
+
xml << %Q{ webapproot="#{replace_entities(webapproot)}"} if (webapproot)
|
130
|
+
xml << '>'
|
131
|
+
@headers.each do |header|
|
132
|
+
xml << header.to_xml
|
133
|
+
end
|
134
|
+
xml << '</Headers>'
|
135
|
+
xml
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# When using htmlform, this represents the login form information.
|
140
|
+
class Field
|
141
|
+
# The name of the HTML field (form parameter).
|
142
|
+
attr_reader :name
|
143
|
+
# The value of the HTML field (form parameter).
|
144
|
+
attr_reader :value
|
145
|
+
# The type of the HTML field (form parameter).
|
146
|
+
attr_reader :type
|
147
|
+
# Is the HTML field (form parameter) dynamically generated? If so,
|
148
|
+
# the login page is requested and the value of the field is extracted
|
149
|
+
# from the response.
|
150
|
+
attr_reader :dynamic
|
151
|
+
# If the HTML field (form parameter) is a radio button, checkbox or select
|
152
|
+
# field, this flag determines if the field should be checked (selected).
|
153
|
+
attr_reader :checked
|
154
|
+
|
155
|
+
# TODO
|
156
|
+
end
|
157
|
+
|
158
|
+
# When using htmlform, this represents the login form information.
|
159
|
+
class HTMLForm
|
160
|
+
# The name of the form being submitted.
|
161
|
+
attr_reader :name
|
162
|
+
# The HTTP action (URL) through which to submit the login form.
|
163
|
+
attr_reader :action
|
164
|
+
# The HTTP request method with which to submit the form.
|
165
|
+
attr_reader :method
|
166
|
+
# The HTTP encoding type with which to submit the form.
|
167
|
+
attr_reader :enctype
|
168
|
+
|
169
|
+
# TODO
|
170
|
+
end
|
171
|
+
|
172
|
+
# When using htmlform, this represents the login form information.
|
173
|
+
class HTMLForms
|
174
|
+
# The URL of the login page containing the login form.
|
175
|
+
attr_reader :parentpage
|
176
|
+
# A regular expression used to match against the response to identify
|
177
|
+
# authentication failures.
|
178
|
+
attr_reader :soft403
|
179
|
+
# Base URL of the application for which the form authentication applies.
|
180
|
+
attr_reader :webapproot
|
181
|
+
|
182
|
+
# TODO
|
183
|
+
end
|
184
|
+
|
185
|
+
# When using ssh-key, this represents the PEM-format keypair information.
|
186
|
+
class PEMKey
|
187
|
+
# TODO
|
188
|
+
end
|
189
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Nexpose
|
2
|
+
class APIError < ::RuntimeError
|
3
|
+
attr_accessor :req, :reason
|
4
|
+
|
5
|
+
def initialize(req, reason = '')
|
6
|
+
@req = req
|
7
|
+
@reason = reason
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_s
|
11
|
+
"NexposeAPI: #{@reason}"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class AuthenticationFailed < APIError
|
16
|
+
def initialize(req)
|
17
|
+
@req = req
|
18
|
+
@reason = "Login Failed"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|