nexpose 0.9.8 → 1.0.0

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,178 @@
1
+ module Nexpose
2
+
3
+ # Object that represents administrative credentials to be used
4
+ # during a scan. When retrieved from an existing site configuration
5
+ # the credentials will be returned as a security blob and can only
6
+ # be passed back as is during a Site Save operation. This object
7
+ # can only be used to create a new set of credentials.
8
+ #
9
+ class SiteCredentials < Credential
10
+
11
+ # Unique identifier of the credential on the Nexpose console.
12
+ attr_accessor :id
13
+ # The service for these credentials.
14
+ attr_accessor :service
15
+ # The host for these credentials.
16
+ attr_accessor :host_restriction
17
+ # The port on which to use these credentials.
18
+ attr_accessor :port_restriction
19
+ # The password
20
+ attr_accessor :password
21
+ # The name
22
+ attr_accessor :name
23
+ # is this credential enable on site or not.
24
+ attr_accessor :enabled
25
+ # the description of credential
26
+ attr_accessor :description
27
+ # domain of the service
28
+ attr_accessor :domain
29
+ # database of the service
30
+ attr_accessor :database
31
+ # The type of privilege escalation to use (sudo/su)
32
+ # Permission elevation type. See Nexpose::Credential::ElevationType.
33
+ attr_accessor :permission_elevation_type
34
+ # The userid to use when escalating privileges (optional)
35
+ attr_accessor :permission_elevation_user
36
+ # The password to use when escalating privileges (optional)
37
+ attr_accessor :permission_elevation_password
38
+ # The authentication type to use with SNMP v3 credentials
39
+ attr_accessor :authentication_type
40
+ # The privacy/encryption type to use with SNMP v3 credentials
41
+ attr_accessor :privacy_type
42
+ # The privacy/encryption pass phrase to use with SNMP v3 credentials
43
+ attr_accessor :privacy_password
44
+ # the user name to be used in service
45
+ attr_accessor :user_name
46
+ # the notes password
47
+ attr_accessor :notes_id_password
48
+ # use windows auth
49
+ attr_accessor :use_windows_auth
50
+ # sid for oracle
51
+ attr_accessor :sid
52
+ #for ssh public key require pem format private key
53
+ attr_accessor :pem_format_private_key
54
+ # for snmp v1/v2
55
+ attr_accessor :community_name
56
+ # scope of credential
57
+ attr_accessor :scope
58
+
59
+ #Create a credential object using name, id, description, host and port
60
+ def self.for_service(name, id = -1, desc = nil, host = nil, port = nil, service = Credential::Service::CIFS)
61
+ cred = new
62
+ cred.name = name
63
+ cred.id = id.to_i
64
+ cred.enabled = true
65
+ cred.description = desc
66
+ cred.host_restriction = host
67
+ cred.port_restriction = port
68
+ cred.service = service
69
+ cred.scope = Credential::Scope::SITE_SPECIFIC
70
+ cred
71
+ end
72
+
73
+ # Load an credential from the provided console.
74
+ #
75
+ # @param [Connection] nsc Active connection to a Nexpose console.
76
+ # @param [String] id Unique identifier of an site.
77
+ # @param [String] id Unique identifier of an credential.
78
+ # @return [SiteCredential] The requested credential of site, if found.
79
+ #
80
+ def self.load(nsc, site_id, credential_id)
81
+ uri = "/api/2.1/sites/#{site_id}/credentials/#{credential_id}"
82
+ resp = AJAX.get(nsc, uri, AJAX::CONTENT_TYPE::JSON)
83
+ hash = JSON.parse(resp, symbolize_names: true)
84
+ new.object_from_hash(nsc, hash)
85
+ end
86
+
87
+ # Copy an existing configuration from a Nexpose instance.
88
+ # Returned object will reset the credential ID and append "Copy" to the existing
89
+ # name.
90
+ #
91
+ # @param [Connection] connection Connection to the security console.
92
+ # @param [String] id Unique identifier of an site.
93
+ # @param [String] id Unique identifier of an credential.
94
+ # @return [SiteCredentials] Site credential loaded from a Nexpose console.
95
+ #
96
+ def self.copy(connection, site_id, credential_id)
97
+ siteCredential = self.load(connection, site_id, credential_id)
98
+ siteCredential.id = -1
99
+ siteCredential.name = "#{siteCredential.name} Copy"
100
+ siteCredential
101
+ end
102
+
103
+ # Copy an existing configuration from a site credential.
104
+ # Returned object will reset the credential ID and append "Copy" to the existing
105
+ # name.
106
+ #
107
+ # @param [siteCredential] site credential to be copied.
108
+ # @return [SiteCredentials] modified.
109
+ #
110
+ def self.copy(siteCredential)
111
+ siteCredential.id = -1
112
+ siteCredential.name = "#{siteCredential.name} Copy"
113
+ siteCredential
114
+ end
115
+
116
+ def to_json
117
+ JSON.generate(to_h)
118
+ end
119
+
120
+ def to_h
121
+ { id: id,
122
+ service: service,
123
+ host_restriction: host_restriction,
124
+ port_restriction: port_restriction,
125
+ password: password,
126
+ name: name,
127
+ enabled: enabled,
128
+ description: description,
129
+ domain: domain,
130
+ database: database,
131
+ permission_elevation_type: permission_elevation_type,
132
+ permission_elevation_user: permission_elevation_user,
133
+ permission_elevation_password: permission_elevation_password,
134
+ authentication_type: authentication_type,
135
+ privacy_type: privacy_type,
136
+ privacy_password: privacy_password,
137
+ user_name: user_name,
138
+ notes_id_password: notes_id_password,
139
+ use_windows_auth: use_windows_auth,
140
+ sid: sid,
141
+ pem_format_private_key: pem_format_private_key,
142
+ community_name: community_name,
143
+ scope: scope
144
+ }
145
+ end
146
+
147
+ def ==(other)
148
+ eql?(other)
149
+ end
150
+
151
+ def eql?(other)
152
+ id.eql?(other.id) &&
153
+ service.eql?(other.service) &&
154
+ host_restriction.eql?(other.host_restriction) &&
155
+ port_restriction.eql?(other.port_restriction) &&
156
+ password.eql?(other.password) &&
157
+ name.eql?(other.name) &&
158
+ enabled.eql?(other.enabled) &&
159
+ description.eql?(other.description) &&
160
+ domain.eql?(other.domain) &&
161
+ database.eql?(other.database) &&
162
+ permission_elevation_type.eql?(other.permission_elevation_type) &&
163
+ permission_elevation_user.eql?(other.permission_elevation_user) &&
164
+ permission_elevation_password.eql?(other.permission_elevation_password) &&
165
+ authentication_type.eql?(other.authentication_type) &&
166
+ privacy_type.eql?(other.privacy_type) &&
167
+ privacy_password.eql?(other.privacy_password) &&
168
+ user_name.eql?(other.user_name) &&
169
+ notes_id_password.eql?(other.notes_id_password) &&
170
+ use_windows_auth.eql?(other.use_windows_auth) &&
171
+ sid.eql?(other.sid) &&
172
+ pem_format_private_key.eql?(other.pem_format_private_key) &&
173
+ community_name.eql?(other.community_name) &&
174
+ scope.eql?(other.scope)
175
+ end
176
+
177
+ end
178
+ end
data/lib/nexpose/tag.rb CHANGED
@@ -218,7 +218,48 @@ module Nexpose
218
218
  end
219
219
 
220
220
  @color = hex
221
- end
221
+ end
222
+
223
+ # Create list of tag objects from hash
224
+ def self.load_tags(tags)
225
+ unless tags.nil?
226
+ tags = tags.map do |hash|
227
+ self.create(hash)
228
+ end
229
+ end
230
+ tags
231
+ end
232
+
233
+ # Create tag object from hash
234
+ def self.create(hash)
235
+ attributes = hash[:attributes]
236
+ color = attributes.find { |attr| attr[:tag_attribute_name] == 'COLOR' }
237
+ color = color[:tag_attribute_value] if color
238
+ source = attributes.find { |attr| attr[:tag_attribute_name] == 'SOURCE' }
239
+ source = source[:tag_attribute_value] if source
240
+ tag = Tag.new(hash[:tag_name], hash[:tag_type], hash[:tag_id])
241
+ tag.color = color
242
+ tag.source = source
243
+ tag
244
+ end
245
+
246
+ def to_h
247
+ {
248
+ tag_id: id,
249
+ tag_name: name,
250
+ tag_type: type,
251
+ attributes:[
252
+ {
253
+ tag_attribute_name: "COLOR",
254
+ tag_attribute_value: color
255
+ },
256
+ {
257
+ tag_attribute_name: "SOURCE",
258
+ tag_attribute_value: source
259
+ }
260
+ ]
261
+ }
262
+ end
222
263
 
223
264
  # Creates and saves a tag to Nexpose console
224
265
  #
data/lib/nexpose/util.rb CHANGED
@@ -1,5 +1,4 @@
1
1
  module Nexpose
2
-
3
2
  module Sanitize
4
3
  def replace_entities(str)
5
4
  str.to_s.gsub(/&/, '&amp;').gsub(/'/, '&apos;').gsub(/"/, '&quot;').gsub(/</, '&lt;').gsub(/>/, '&gt;')
@@ -7,21 +6,18 @@ module Nexpose
7
6
  end
8
7
 
9
8
  module XMLUtils
10
-
11
9
  def parse_xml(xml)
12
10
  ::REXML::Document.new(xml.to_s)
13
11
  end
14
12
 
15
13
  def make_xml(name, opts = {}, data = '', append_session_id = true)
16
14
  xml = REXML::Element.new(name)
17
- if @session_id and append_session_id
15
+ if @session_id && append_session_id
18
16
  xml.attributes['session-id'] = @session_id
19
17
  end
20
18
 
21
19
  opts.keys.each do |k|
22
- if opts[k] != nil
23
- xml.attributes[k] = "#{opts[k]}"
24
- end
20
+ xml.attributes[k] = "#{opts[k]}" unless opts[k].nil?
25
21
  end
26
22
 
27
23
  xml.text = data
@@ -54,16 +50,15 @@ module Nexpose
54
50
  # @return [IPRange|HostName] Valid class, if it can be converted.
55
51
  #
56
52
  def convert(asset)
57
- begin
58
- # Use IPAddr construtor validation to see if it's an IP.
59
- IPAddr.new(asset)
60
- IPRange.new(asset)
61
- rescue ArgumentError => e
62
- if e.message == 'invalid address'
63
- HostName.new(asset)
64
- else
65
- raise "Unable to parse asset: '#{asset}'. #{e.message}"
66
- end
53
+ ips = asset.split(' - ')
54
+ IPAddr.new(ips[0])
55
+ IPAddr.new(ips[1]) if ips[1]
56
+ IPRange.new(ips[0], ips[1])
57
+ rescue ArgumentError => e
58
+ if e.message == 'invalid address'
59
+ HostName.new(asset)
60
+ else
61
+ raise "Unable to parse asset: '#{asset}'. #{e.message}"
67
62
  end
68
63
  end
69
64
 
@@ -1,4 +1,4 @@
1
1
  module Nexpose
2
2
  # The latest version of the Nexpose gem
3
- VERSION = '0.9.8'
3
+ VERSION = '1.0.0'
4
4
  end
@@ -0,0 +1,103 @@
1
+ module Nexpose
2
+ class Wait
3
+ attr_reader :error_message, :ready, :retry_count, :timeout, :polling_interval
4
+
5
+ def initialize(retry_count: nil, timeout: nil, polling_interval: nil)
6
+ @error_message = 'Default General Failure in Nexpose::Wait'
7
+ @ready = false
8
+ @retry_count = retry_count.to_i
9
+ @timeout = timeout
10
+ @polling_interval = polling_interval
11
+ end
12
+
13
+ def ready?
14
+ @ready
15
+ end
16
+
17
+ def for_report(nexpose_connection:, report_id:)
18
+ poller = Nexpose::Poller.new(timeout: @timeout, polling_interval: @polling_interval)
19
+ poller.wait(report_status_proc(nexpose_connection: nexpose_connection, report_id: report_id))
20
+ @ready = true
21
+ rescue TimeoutError
22
+ retry if timeout_retry?
23
+ @error_message = "Timeout Waiting for Report to Generate - Report Config ID: #{report_id}"
24
+ rescue NoMethodError => error
25
+ @error_message = "Error Report Config ID: #{report_id} :: Report Probably Does Not Exist :: #{error}"
26
+ rescue => error
27
+ @error_message = "Error Report Config ID: #{report_id} :: #{error}"
28
+ end
29
+
30
+ def for_integration(nexpose_connection:, scan_id:, status: 'finished')
31
+ poller = Nexpose::Poller.new(timeout: @timeout, polling_interval: @polling_interval)
32
+ poller.wait(integration_status_proc(nexpose_connection: nexpose_connection, scan_id: scan_id, status: status))
33
+ @ready = true
34
+ rescue TimeoutError
35
+ retry if timeout_retry?
36
+ @error_message = "Timeout Waiting for Integration Status of '#{status}' - Scan ID: #{scan_id}"
37
+ rescue Nexpose::APIError => error
38
+ @error_message = "API Error Waiting for Integration Scan ID: #{scan_id} :: #{error.req.error}"
39
+ end
40
+
41
+ def for_judgment(proc:, desc:)
42
+ poller = Nexpose::Poller.new(timeout: @timeout, polling_interval: @polling_interval)
43
+ poller.wait(proc)
44
+ @ready = true
45
+ rescue TimeoutError
46
+ retry if timeout_retry?
47
+ @error_message = "Timeout Waiting for Judgment to Judge. #{desc}"
48
+ end
49
+
50
+ private
51
+
52
+ def report_status_proc(nexpose_connection:, report_id:)
53
+ Proc.new { nexpose_connection.last_report(report_id).status == 'Generated' }
54
+ end
55
+
56
+ def integration_status_proc(nexpose_connection:, scan_id:, status:)
57
+ Proc.new { nexpose_connection.scan_status(scan_id).downcase == status.downcase }
58
+ end
59
+
60
+ def timeout_retry?
61
+ if @retry_count > 0
62
+ @retry_count -= 1
63
+ true
64
+ else
65
+ false
66
+ end
67
+ end
68
+ end
69
+
70
+ class Poller
71
+ ## Stand alone object to handle waiting logic.
72
+ attr_reader :timeout, :polling_interval, :poll_begin
73
+
74
+ def initialize(timeout: nil, polling_interval: nil)
75
+ global_timeout = set_global_timeout
76
+ @timeout = timeout.nil? ? global_timeout : timeout
77
+
78
+ global_polling = set_polling_interval
79
+ @polling_interval = polling_interval.nil? ? global_polling : polling_interval
80
+ end
81
+
82
+ def wait(condition)
83
+ @poll_begin = Time.now
84
+ loop do
85
+ break if condition.call
86
+ raise TimeoutError if @poll_begin + @timeout < Time.now
87
+ sleep @polling_interval
88
+ end
89
+ end
90
+
91
+ private
92
+
93
+ def set_global_timeout
94
+ default_timeout = 120
95
+ ENV['GLOBAL_TIMEOUT'].nil? ? default_timeout : ENV['GLOBAL_TIMEOUT']
96
+ end
97
+
98
+ def set_polling_interval
99
+ default_polling = 1
100
+ ENV['GLOBAL_POLLING_INTERVAL'].nil? ? default_polling : ENV['GLOBAL_POLLING_INTERVAL']
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,252 @@
1
+ module Nexpose
2
+
3
+ # Object that represents web credential defined in site configuration.
4
+ module WebCredentials
5
+
6
+ module WebAppAuthType
7
+ HTML_FORM = 'htmlform' # Represent HTML form credentials.
8
+ HTTP_HEADER = 'httpheaders' # Represent HTTP header credentials.
9
+ end
10
+
11
+ # Object that represents Header name-value pairs, associated with Web Session Authentication.
12
+ #
13
+ class Header
14
+
15
+ # Name, one per Header
16
+ attr_reader :name
17
+ # Value, one per Header
18
+ attr_reader :value
19
+
20
+ # Construct with name value pair
21
+ def initialize(name, value)
22
+ @name = name
23
+ @value = value
24
+ end
25
+
26
+ def to_json
27
+ JSON.generate(to_h)
28
+ end
29
+
30
+ def to_h
31
+ header = Hash.new
32
+ header[@name] = @value
33
+ header
34
+ end
35
+ end
36
+
37
+ # Object that represents Headers, associated with Web Session Authentication.
38
+ #
39
+ class Headers < APIObject
40
+
41
+ # A regular expression used to match against the response to identify authentication failures.
42
+ attr_reader :soft403Pattern
43
+ # Base URL of the application for which the form authentication applies.
44
+ attr_reader :baseURL
45
+ # When using HTTP headers, this represents the set of headers to pass with the authentication request.
46
+ attr_reader :headers
47
+ # name of the html header
48
+ attr_reader :name
49
+ # is this enable for the site configuration
50
+ attr_accessor :enabled
51
+ #service type of header
52
+ attr_reader :service
53
+ # id of the header
54
+ attr_reader :id
55
+
56
+
57
+ def initialize(name, baseURL, soft403Pattern, id = -1, enabled = true)
58
+ @headers = {}
59
+ @name = name
60
+ @baseURL = baseURL
61
+ @soft403Pattern = soft403Pattern
62
+ @service = WebAppAuthType::HTTP_HEADER
63
+ @enabled = enabled
64
+ @id = id
65
+ end
66
+
67
+ def add_header(header)
68
+ @headers = @headers.merge(header.to_h)
69
+ end
70
+
71
+ def to_json
72
+ JSON.generate(to_h)
73
+ end
74
+
75
+ def to_h
76
+ { id: id,
77
+ service: service,
78
+ enabled: enabled,
79
+ name: name,
80
+ headers: headers,
81
+ baseURL: baseURL,
82
+ soft403Pattern: soft403Pattern
83
+ }
84
+ end
85
+
86
+ def ==(other)
87
+ eql?(other)
88
+ end
89
+
90
+ def eql?(other)
91
+ id.eql?(other.id) &&
92
+ service.eql?(other.service) &&
93
+ enabled.eql?(other.enabled) &&
94
+ name.eql?(other.name) &&
95
+ headers.eql?(other.headers) &&
96
+ baseURL.eql?(other.baseURL) &&
97
+ soft403Pattern.eql?(other.soft403Pattern)
98
+ end
99
+ end
100
+
101
+ # When using HTML form, this represents the login form information.
102
+ #
103
+ class Field
104
+ # The name of the HTML field (form parameter).
105
+ attr_reader :name
106
+ # The value of the HTML field (form parameter).
107
+ attr_reader :value
108
+ # The type of the HTML field (form parameter).
109
+ attr_reader :type
110
+ # Is the HTML field (form parameter) dynamically generated? If so,
111
+ # the login page is requested and the value of the field is extracted
112
+ # from the response.
113
+ attr_reader :dynamic
114
+ # If the HTML field (form parameter) is a radio button, checkbox or select
115
+ # field, this flag determines if the field should be checked (selected).
116
+ attr_reader :checked
117
+
118
+ def initialize(name, value, type, dynamic, checked)
119
+ @name = name
120
+ @value = value
121
+ @type = type
122
+ @dynamic = dynamic
123
+ @checked = checked
124
+ end
125
+
126
+ def to_json
127
+ JSON.generate(to_h)
128
+ end
129
+
130
+ def to_h
131
+ {
132
+ value: value,
133
+ type: type,
134
+ name: name,
135
+ dynamic: dynamic,
136
+ checked: checked
137
+ }
138
+ end
139
+ end
140
+
141
+ # When using HTML form, this represents the login form information.
142
+ #
143
+ class HTMLForm
144
+
145
+ # The name of the form being submitted.
146
+ attr_reader :name
147
+ # The HTTP action (URL) through which to submit the login form.
148
+ attr_reader :action
149
+ # The HTTP request method with which to submit the form.
150
+ attr_reader :method
151
+ # The HTTP encoding type with which to submit the form.
152
+ attr_reader :encType
153
+ # The fields in the HTML Form
154
+ attr_reader :fields
155
+
156
+ def initialize(name, action, method, encType)
157
+ @name = name
158
+ @action = action
159
+ @method = method
160
+ @encType = encType
161
+ @fields = []
162
+ end
163
+
164
+ def add_field(field)
165
+ @fields << field.to_h
166
+ end
167
+
168
+ def to_json
169
+ JSON.generate(to_h)
170
+ end
171
+
172
+ def to_h
173
+ { name: name,
174
+ action: action,
175
+ method: method,
176
+ encType: encType,
177
+ fields: fields,
178
+ parentPage: action
179
+ }
180
+ end
181
+ end
182
+
183
+ # When using HTML form, this represents the login form information.
184
+ #
185
+ class HTMLForms < APIObject
186
+
187
+ # A regular expression used to match against the response to identify authentication failures.
188
+ attr_reader :soft403Pattern
189
+ # Base URL of the application for which the form authentication applies.
190
+ attr_reader :baseURL
191
+ # The URL of the login page containing the login form.
192
+ attr_reader :loginURL
193
+ # name of the html header
194
+ attr_reader :name
195
+ # is this enable for the site configuration
196
+ attr_accessor :enabled
197
+ #service type of header
198
+ attr_reader :service
199
+ # id of the header
200
+ attr_reader :id
201
+ # The forms to authenticate with
202
+ attr_reader :form
203
+
204
+ def initialize(name, baseURL, loginURL, soft403Pattern, id = -1, enabled = true)
205
+ @name = name
206
+ @baseURL = baseURL
207
+ @loginURL = loginURL
208
+ @soft403Pattern = soft403Pattern
209
+ @service = WebAppAuthType::HTML_FORM
210
+ @enabled = enabled
211
+ @id = id
212
+ end
213
+
214
+ def add_html_form(html_form)
215
+ @form = html_form
216
+ end
217
+
218
+ def to_json
219
+ JSON.generate(to_h)
220
+ end
221
+
222
+ def to_h
223
+ { id: id,
224
+ service: service,
225
+ enabled: enabled,
226
+ name: name,
227
+ form: form.to_h,
228
+ baseURL: baseURL,
229
+ loginURL: loginURL,
230
+ soft403Pattern: soft403Pattern
231
+ }
232
+ end
233
+
234
+ def ==(other)
235
+ eql?(other)
236
+ end
237
+
238
+ def eql?(other)
239
+ id.eql?(other.id) &&
240
+ service.eql?(other.service) &&
241
+ enabled.eql?(other.enabled) &&
242
+ name.eql?(other.name) &&
243
+ form.eql?(other.form) &&
244
+ baseURL.eql?(other.baseURL) &&
245
+ loginURL.eql?(other.loginURL) &&
246
+ soft403Pattern.eql?(other.soft403Pattern)
247
+ end
248
+
249
+ end
250
+ end
251
+ end
252
+