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