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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 6c5d157c20856a164ee2982bbf4d66895bbf2338
4
- data.tar.gz: 8ec9a5560d29c9e19449b19e2a93a307877d5fcd
3
+ metadata.gz: 0443897c1940a9c3099ecc230589e7925fca9697
4
+ data.tar.gz: e31a6036c4170c0fa796b961973601bc9d5db6bb
5
5
  SHA512:
6
- metadata.gz: 7ecf67806e6577ccf7072261a27fb2cfec3f5b3b9e7aae24fad579900309c3cf73869437adaca9fccda5038f75226a50286bbc9a001a4c2dfb7d7db1ebb1ea3a
7
- data.tar.gz: b112151d6331a44c6dfe4767e365944e07fd6d10386a22be7fe0e6ed68af75058f51e193aef38e3249dca4ed32e05f3a97523ce34120e72ff56cd0b6a8757510
6
+ metadata.gz: e9b887b9f2564478be53d7b45271d1035fef4f51d19e87f5a729ce0423d1571eb0c9e145f3eba7726992a936596e7db5f6f8b2fbe174f98444ab32ef44f77566
7
+ data.tar.gz: 636f2e44bb5b141a5d3d670b4f8290009af0e1f1dc5d2aeea6b1e2c77687f99d6483f6d063fbd696e1f558ac679dc58915dd896103cc7baa8786ddba5793b3c7
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- nexpose (0.9.8)
4
+ nexpose (1.0.0)
5
5
  rex (~> 2.0, >= 2.0.8)
6
6
 
7
7
  GEM
data/lib/nexpose.rb CHANGED
@@ -57,25 +57,27 @@ require 'rex/mime'
57
57
  require 'ipaddr'
58
58
  require 'json'
59
59
  require 'cgi'
60
+ require 'nexpose/api'
61
+ require 'nexpose/json_serializer'
60
62
  require 'nexpose/error'
61
63
  require 'nexpose/util'
62
64
  require 'nexpose/alert'
63
65
  require 'nexpose/ajax'
64
- require 'nexpose/api'
65
66
  require 'nexpose/api_request'
66
67
  require 'nexpose/asset'
67
68
  require 'nexpose/common'
68
69
  require 'nexpose/console'
69
70
  require 'nexpose/credential'
70
- require 'nexpose/site_credential'
71
+ require 'nexpose/site_credentials'
71
72
  require 'nexpose/shared_credential'
73
+ require 'nexpose/web_credentials'
72
74
  require 'nexpose/data_table'
73
75
  require 'nexpose/device'
74
- require 'nexpose/discovery'
75
- require 'nexpose/discovery/filter'
76
76
  require 'nexpose/engine'
77
77
  require 'nexpose/external'
78
78
  require 'nexpose/filter'
79
+ require 'nexpose/discovery'
80
+ require 'nexpose/discovery/filter'
79
81
  require 'nexpose/global_settings'
80
82
  require 'nexpose/group'
81
83
  require 'nexpose/dag'
@@ -87,6 +89,7 @@ require 'nexpose/report_template'
87
89
  require 'nexpose/role'
88
90
  require 'nexpose/scan'
89
91
  require 'nexpose/scan_template'
92
+ require 'nexpose/shared_secret'
90
93
  require 'nexpose/silo'
91
94
  require 'nexpose/silo_profile'
92
95
  require 'nexpose/site'
@@ -100,6 +103,7 @@ require 'nexpose/vuln_exception'
100
103
  require 'nexpose/connection'
101
104
  require 'nexpose/maint'
102
105
  require 'nexpose/version'
106
+ require 'nexpose/wait'
103
107
 
104
108
  module Nexpose
105
109
 
data/lib/nexpose/ajax.rb CHANGED
@@ -8,6 +8,9 @@ module Nexpose
8
8
  module AJAX
9
9
  module_function
10
10
 
11
+ API_PATTERN = %r{/api/(?<version>[\d\.]+)}
12
+ private_constant :API_PATTERN
13
+
11
14
  # Content type strings acceptect by Nexpose.
12
15
  #
13
16
  module CONTENT_TYPE
@@ -157,15 +160,37 @@ module Nexpose
157
160
  if response.header['location'] =~ /login/
158
161
  raise Nexpose::AuthenticationFailed.new(response)
159
162
  else
160
- req_type = request.class.name.split('::').last.upcase
161
- raise Nexpose::APIError.new(response, "#{req_type} request to #{request.path} failed. #{request.body}", response.code)
163
+ raise get_api_error(request, response)
162
164
  end
163
165
  else
164
- req_type = request.class.name.split('::').last.upcase
165
- raise Nexpose::APIError.new(response, "#{req_type} request to #{request.path} failed. #{request.body}", response.code)
166
+ raise get_api_error(request, response)
166
167
  end
167
168
  end
168
169
 
170
+ def get_api_error(request, response)
171
+ req_type = request.class.name.split('::').last.upcase
172
+ error_message = get_error_message(request, response)
173
+ Nexpose::APIError.new(response, "#{req_type} request to #{request.path} failed. #{error_message}", response.code)
174
+ end
175
+
176
+ # Get the version of the api target by request
177
+ #
178
+ # @param [HTTPRequest] request
179
+ def get_request_api_version(request)
180
+ matches = request.path.match(API_PATTERN)
181
+ matches[:version].to_f
182
+ rescue
183
+ 0.0
184
+ end
185
+
186
+ # Get an error message from the response body if the request url api version
187
+ # is 2.1 or greater otherwise use the request body
188
+ def get_error_message(request, response)
189
+ version = get_request_api_version(request)
190
+
191
+ (version >= 2.1 && response.body) ? "response body: #{response.body}" : "request body: #{request.body}"
192
+ end
193
+
169
194
  # Execute a block of code while presenving the preferences for any
170
195
  # underlying table being accessed. Use this method when accessing data
171
196
  # tables which are present in the UI to prevent existing row preferences
data/lib/nexpose/alert.rb CHANGED
@@ -1,107 +1,29 @@
1
1
  module Nexpose
2
2
 
3
- # Alert parent object.
4
- # The three alert types should be wrapped in this object to store data.
5
- #
6
- class Alert
7
-
8
- # Name for this alert.
9
- attr_accessor :name
10
- # Whether or not this alert is currently active.
11
- attr_accessor :enabled
12
- # Send at most this many alerts per scan.
13
- attr_accessor :max_alerts
14
- # Send alerts based upon scan status.
15
- attr_accessor :scan_filter
16
- # Send alerts based upon vulnerability finding status.
17
- attr_accessor :vuln_filter
18
- # Alert type and its configuration. One of SMTPAlert, SyslogAlert, SNMPAlert
19
- attr_accessor :type
20
-
21
- def initialize(name, enabled = 1, max_alerts = -1)
22
- @name, @enabled, @max_alerts = name, enabled, max_alerts
23
- end
24
-
25
- def as_xml
26
- xml = REXML::Element.new('Alert')
27
- xml.attributes['name'] = @name
28
- xml.attributes['enabled'] = @enabled
29
- xml.attributes['maxAlerts'] = @max_alerts
30
- xml.add_element(scan_filter.as_xml)
31
- xml.add_element(vuln_filter.as_xml)
32
- xml.add_element(type.as_xml)
33
- xml
34
- end
35
-
36
- def to_xml
37
- as_xml.to_s
38
- end
39
-
40
- # Parse a response from a Nexpose console into a valid Alert object.
41
- #
42
- # @param [REXML::Document] rexml XML document to parse.
43
- # @return [Alert] Alert object represented by the XML.
44
- #
45
- def self.parse(rexml)
46
- name = rexml.attributes['name']
47
- rexml.elements.each("//Alert[@name='#{name}']") do |xml|
48
- alert = new(name,
49
- xml.attributes['enabled'].to_i,
50
- xml.attributes['maxAlerts'].to_i)
51
- alert.scan_filter = ScanFilter.parse(REXML::XPath.first(xml, "//Alert[@name='#{name}']/scanFilter"))
52
- alert.vuln_filter = VulnFilter.parse(REXML::XPath.first(xml, "//Alert[@name='#{name}']/vulnFilter"))
53
- if (type = REXML::XPath.first(xml, "//Alert[@name='#{name}']/smtpAlert"))
54
- alert.type = SMTPAlert.parse(type)
55
- elsif (type = REXML::XPath.first(xml, "//Alert[@name='#{name}']/syslogAlert"))
56
- alert.type = SyslogAlert.parse(type)
57
- elsif (type = REXML::XPath.first(xml, "//Alert[@name='#{name}']/snmpAlert"))
58
- alert.type = SNMPAlert.parse(type)
59
- end
60
- return alert
61
- end
62
- nil
63
- end
64
- end
65
-
66
3
  # Scan filter for alerting.
67
4
  # Set values to 1 to enable and 0 to disable.
68
- #
69
5
  class ScanFilter
6
+ include JsonSerializer
70
7
  # Scan events to alert on.
71
8
  attr_accessor :start, :stop, :fail, :resume, :pause
72
9
 
73
10
  def initialize(start = 0, stop = 0, fail = 0, resume = 0, pause = 0)
74
- @start, @stop, @fail, @resume, @pause = start, stop, fail, resume, pause
75
- end
76
-
77
- def as_xml
78
- xml = REXML::Element.new('scanFilter')
79
- xml.attributes['scanStart'] = @start
80
- xml.attributes['scanStop'] = @stop
81
- xml.attributes['scanFailed'] = @fail
82
- xml.attributes['scanResumed'] = @resume
83
- xml.attributes['scanPaused'] = @pause
84
- xml
85
- end
86
-
87
- def to_xml
88
- as_xml.to_s
11
+ @start, @stop, @fail, @resume, @pause = start.to_i, stop.to_i, fail.to_i, resume.to_i, pause.to_i
89
12
  end
90
13
 
91
- def self.parse(xml)
92
- new(xml.attributes['scanStart'].to_i,
93
- xml.attributes['scanStop'].to_i,
94
- xml.attributes['scanFailed'].to_i,
95
- xml.attributes['scanResumed'].to_i,
96
- xml.attributes['scanPaused'].to_i)
14
+ def self.json_initializer(filter)
15
+ new(filter[:start] ? 1 : 0,
16
+ filter[:stop] ? 1 : 0,
17
+ filter[:failed] ? 1 : 0,
18
+ filter[:resume] ? 1 : 0,
19
+ filter[:pause] ? 1 : 0)
97
20
  end
98
21
  end
99
22
 
100
23
  # Vulnerability filtering for alerting.
101
24
  # Set values to 1 to enable and 0 to disable.
102
- #
103
25
  class VulnFilter
104
-
26
+ include JsonSerializer
105
27
  # Only alert on vulnerability findings with a severity level greater than this level.
106
28
  # Range is 0 to 10.
107
29
  # Values in the UI correspond as follows:
@@ -114,138 +36,199 @@ module Nexpose
114
36
  attr_accessor :confirmed, :unconfirmed, :potential
115
37
 
116
38
  def initialize(severity = 1, confirmed = 1, unconfirmed = 1, potential = 1)
117
- @severity, @confirmed, @unconfirmed, @potential = severity, confirmed, unconfirmed, potential
118
- end
119
-
120
- def as_xml
121
- xml = REXML::Element.new('vulnFilter')
122
- xml.attributes['severityThreshold'] = @severity
123
- xml.attributes['confirmed'] = @confirmed
124
- xml.attributes['unconfirmed'] = @unconfirmed
125
- xml.attributes['potential'] = @potential
126
- xml
127
- end
128
-
129
- def to_xml
130
- as_xml.to_s
39
+ @severity, @confirmed = severity.to_i, confirmed.to_i
40
+ @unconfirmed, @potential = unconfirmed.to_i, potential.to_i
131
41
  end
132
42
 
133
- def self.parse(xml)
134
- new(xml.attributes['severityThreshold'].to_i,
135
- xml.attributes['confirmed'].to_i,
136
- xml.attributes['unconfirmed'].to_i,
137
- xml.attributes['potential'].to_i)
43
+ def self.json_initializer(filter)
44
+ new(filter[:severity] ? 1 : 0,
45
+ filter[:unconfirmed] ? 1 : 0,
46
+ filter[:confirmed] ? 1 : 0,
47
+ filter[:potential] ? 1 : 0)
138
48
  end
139
49
  end
140
50
 
141
- # Syslog Alert
142
- # This class should only exist as an element of an Alert.
143
- #
144
- class SyslogAlert
51
+ # Alert base behavior.
52
+ # The supported three alert types should have these properties and behaviors
53
+ module Alert
54
+ include JsonSerializer
55
+ extend TypedAccessor
145
56
 
146
- # The server to sent this alert to.
57
+ # ID for this alert.
58
+ attr_accessor :id
59
+ # Name for this alert.
60
+ attr_accessor :name
61
+ # Whether or not this alert is currently active.
62
+ attr_accessor :enabled
63
+ # Send at most this many alerts per scan.
64
+ attr_accessor :max_alerts
65
+ # Alert type and its configuration. One of SMTPAlert, SyslogAlert, SNMPAlert
66
+ attr_accessor :alert_type
67
+ # Server target the alerts
147
68
  attr_accessor :server
69
+ # Server port
70
+ attr_accessor :server_port
148
71
 
149
- def initialize(server)
150
- @server = server
151
- end
72
+ # Send alerts based upon scan status.
73
+ typed_accessor :scan_filter, ScanFilter
74
+ # Send alerts based upon vulnerability finding status.
75
+ typed_accessor :vuln_filter, VulnFilter
152
76
 
153
- def self.parse(xml)
154
- new(xml.attributes['server'])
77
+ # load a particular site alert
78
+ def self.load(nsc, site_id, alert_id)
79
+ uri = "/api/2.1/site_configurations/#{site_id}/alerts/#{alert_id}"
80
+ resp = AJAX.get(nsc, uri, AJAX::CONTENT_TYPE::JSON)
81
+
82
+ unless resp.to_s == ''
83
+ data = JSON.parse(resp, symbolize_names: true)
84
+ json_initializer(data).deserialize(data)
85
+ end
155
86
  end
156
87
 
157
- def as_xml
158
- xml = REXML::Element.new('syslogAlert')
159
- xml.attributes['server'] = @server
160
- xml
88
+ # load alerts from an array of hashes
89
+ def self.load_alerts(alerts)
90
+ alerts.map { |hash| json_initializer(hash).deserialize(hash) }
161
91
  end
162
92
 
163
- def to_xml
164
- as_xml.to_s
93
+ # load a list of alerts for a given site
94
+ def self.list_alerts(nsc, site_id)
95
+ uri = "/api/2.1/site_configurations/#{site_id}/alerts"
96
+ resp = AJAX.get(nsc, uri, AJAX::CONTENT_TYPE::JSON)
97
+ data = JSON.parse(resp, symbolize_names: true)
98
+ load_alerts(data) unless data.nil?
165
99
  end
166
- end
167
100
 
168
- # SNMP Alert
169
- # This class should only exist as an element of an Alert.
170
- #
171
- class SNMPAlert
101
+ def self.json_initializer(hash)
102
+ create(hash)
103
+ end
172
104
 
173
- # The community string
174
- attr_accessor :community
105
+ def to_h
106
+ to_hash(Hash.new)
107
+ end
175
108
 
176
- # The server to sent this alert
177
- attr_accessor :server
109
+ def to_json
110
+ serialize
111
+ end
178
112
 
179
- def initialize(community, server)
180
- @community = community
181
- @server = server
113
+ # delete an alert from the given site
114
+ def delete(nsc, site_id)
115
+ uri = "/api/2.1/site_configurations/#{site_id}/alerts/#{id}"
116
+ AJAX.delete(nsc, uri, AJAX::CONTENT_TYPE::JSON)
182
117
  end
183
118
 
184
- def self.parse(xml)
185
- new(xml.attributes['community'], xml.attributes['server'])
119
+ # save an alert for a given site
120
+ def save(nsc, site_id)
121
+ validate
122
+ uri = "/api/2.1/site_configurations/#{site_id}/alerts"
123
+ id = AJAX.put(nsc, uri, self.to_json, AJAX::CONTENT_TYPE::JSON)
124
+ @id = id.to_i
186
125
  end
187
126
 
188
- def as_xml
189
- xml = REXML::Element.new('snmpAlert')
190
- xml.attributes['community'] = @community
191
- xml.attributes['server'] = @server
192
- xml
127
+ def validate
128
+ raise ArgumentError.new('Name is a required attribute.') unless @name
129
+ raise ArgumentError.new('Scan filter is a required attribute.') unless @scan_filter
130
+ raise ArgumentError.new('Vuln filter is a required attribute.') unless @vuln_filter
193
131
  end
194
132
 
195
- def to_xml
196
- as_xml.to_s
133
+ private
134
+
135
+ def self.create(hash)
136
+ alert_type = hash[:alert_type]
137
+ raise 'An alert must have an alert type' if alert_type.nil?
138
+ raise 'Alert name cannot be empty.' if !hash.has_key?(:name) || hash[:name].to_s == ''
139
+ raise 'SNMP and Syslog alerts must have a server defined' if ['SNMP', 'Syslog'].include?(alert_type) && hash[:server].to_s == ''
140
+
141
+ case alert_type
142
+ when 'SMTP'
143
+ alert = SMTPAlert.new(hash[:name],
144
+ hash[:sender],
145
+ hash[:server],
146
+ hash[:recipients],
147
+ hash[:enabled],
148
+ hash[:max_alerts],
149
+ hash[:verbose])
150
+ when 'SNMP'
151
+ alert = SNMPAlert.new(hash[:name],
152
+ hash[:community],
153
+ hash[:server],
154
+ hash[:enabled],
155
+ hash[:max_alerts])
156
+ when 'Syslog'
157
+ alert = SyslogAlert.new(hash[:name],
158
+ hash[:server],
159
+ hash[:enabled],
160
+ hash[:max_alerts])
161
+ else
162
+ fail "Unknown alert type: #{alert_type}"
163
+ end
164
+
165
+ alert.scan_filter = ScanFilter.new
166
+ alert.vuln_filter = VulnFilter.new
167
+ alert
197
168
  end
198
169
  end
199
170
 
200
171
  # SMTP (e-mail) Alert
201
- # This class should only exist as an element of an Alert.
202
- #
203
172
  class SMTPAlert
173
+ include Alert
174
+ attr_accessor :recipients, :sender, :verbose
204
175
 
205
- # The e-mail address of the sender.
206
- attr_accessor :sender
207
- # The server to sent this alert.
208
- attr_accessor :server
209
- # Limit the text for mobile devices.
210
- attr_accessor :limit_text
211
- # Array of strings with the e-mail addresses of the intended recipients.
212
- attr_accessor :recipients
176
+ def initialize(name, sender, server, recipients, enabled = 1, max_alerts = -1, verbose = 0)
177
+ unless recipients.is_a?(Array) && recipients.length > 0
178
+ raise 'An SMTP alert must contain an array of recipient emails with at least 1 recipient'
179
+ end
180
+ recipients.each do |recipient|
181
+ unless recipient =~ /^.+@.+\..+$/
182
+ raise "Recipients must contain valid emails, #{recipient} has an invalid format"
183
+ end
184
+ end
213
185
 
214
- def initialize(sender, server, limit_text = 0)
186
+ @alert_type = 'SMTP'
187
+ @name = name
188
+ @enabled = enabled
189
+ @max_alerts = max_alerts
215
190
  @sender = sender
216
191
  @server = server
217
- @limit_text = limit_text
218
- @recipients = []
192
+ @verbose = verbose
193
+ @recipients = recipients.nil? ? [] : recipients
219
194
  end
220
195
 
221
- # Adds a new recipient to the alert.
222
- def add_recipient(recipient)
196
+ def add_email_recipient(recipient)
223
197
  @recipients << recipient
224
198
  end
225
199
 
226
- def as_xml
227
- xml = REXML::Element.new('smtpAlert')
228
- xml.attributes['sender'] = @sender
229
- xml.attributes['server'] = @server
230
- xml.attributes['limitText'] = @limit_text
231
- recipients.each do |recpt|
232
- elem = REXML::Element.new('recipient')
233
- elem.text = recpt
234
- xml.add_element(elem)
235
- end
236
- xml
200
+ def remove_email_recipient(recipient)
201
+ @recipients.delete(recipient)
237
202
  end
203
+ end
204
+
205
+ # SNMP Alert
206
+ class SNMPAlert
207
+ include Alert
208
+ attr_accessor :community
209
+
210
+ def initialize(name, community, server, enabled = 1, max_alerts = -1)
211
+ raise 'SNMP alerts must have a community defined.' if community.nil?
238
212
 
239
- def to_xml
240
- as_xml.to_s
213
+ @alert_type = 'SNMP'
214
+ @name = name
215
+ @enabled = enabled
216
+ @max_alerts = max_alerts
217
+ @community = community
218
+ @server = server
241
219
  end
220
+ end
242
221
 
243
- def self.parse(xml)
244
- alert = new(xml.attributes['sender'], xml.attributes['server'], xml.attributes['limitText'].to_i)
245
- xml.elements.each("//recipient") do |recipient|
246
- alert.recipients << recipient.text
247
- end
248
- alert
222
+ # Syslog Alert
223
+ class SyslogAlert
224
+ include Alert
225
+
226
+ def initialize(name, server, enabled = 1, max_alerts = -1)
227
+ @alert_type = 'Syslog'
228
+ @name = name
229
+ @enabled = enabled
230
+ @max_alerts = max_alerts
231
+ @server = server
249
232
  end
250
233
  end
251
234
  end