nexpose 0.0.9 → 0.0.91
Sign up to get free protection for your applications and to get access to all the features.
- 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
|