nexpose 0.9.8 → 1.0.0

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