nexpose 0.2.8 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,249 @@
1
+ module Nexpose
2
+
3
+ # NexposeAPI module is mixed into the Connection object, and all methods are
4
+ # expected to be called from there.
5
+ #
6
+ module NexposeAPI
7
+ include XMLUtils
8
+
9
+ # Provide a list of all report templates the user can access on the
10
+ # Security Console.
11
+ #
12
+ # @return [Array[ReportTemplateSummary]] List of current report templates.
13
+ #
14
+ def list_report_templates
15
+ r = execute(make_xml('ReportTemplateListingRequest', {}))
16
+ templates = []
17
+ if r.success
18
+ r.res.elements.each('//ReportTemplateSummary') do |template|
19
+ templates << ReportTemplateSummary.parse(template)
20
+ end
21
+ end
22
+ templates
23
+ end
24
+
25
+ alias_method :report_templates, :list_report_templates
26
+
27
+ # Deletes an existing, custom report template.
28
+ # Cannot delete built-in templates.
29
+ #
30
+ # @param [String] template_id Unique identifier of the report template to remove.
31
+ #
32
+ def delete_report_template(template_id)
33
+ AJAX.delete(self, "/data/report/templates/#{template_id}")
34
+ end
35
+ end
36
+
37
+ # Data object for report template summary information.
38
+ # Not meant for use in creating new templates.
39
+ #
40
+ class ReportTemplateSummary
41
+
42
+ # The ID of the report template.
43
+ attr_reader :id
44
+ # The name of the report template.
45
+ attr_reader :name
46
+ # One of: data|document. With a data template, you can export
47
+ # comma-separated value (CSV) files with vulnerability-based data.
48
+ # With a document template, you can create PDF, RTF, HTML, or XML reports
49
+ # with asset-based information.
50
+ attr_reader :type
51
+ # The visibility (scope) of the report template. One of: global|silo
52
+ attr_reader :scope
53
+ # Whether the report template is built-in, and therefore cannot be modified.
54
+ attr_reader :built_in
55
+ # Description of the report template.
56
+ attr_reader :description
57
+
58
+ def initialize(id, name, type, scope, built_in, description)
59
+ @id = id
60
+ @name = name
61
+ @type = type
62
+ @scope = scope
63
+ @built_in = built_in
64
+ @description = description
65
+ end
66
+
67
+ def delete(connection)
68
+ connection.delete_report_template(@id)
69
+ end
70
+
71
+ def self.parse(xml)
72
+ description = nil
73
+ xml.elements.each('description') { |desc| description = desc.text }
74
+ ReportTemplateSummary.new(xml.attributes['id'],
75
+ xml.attributes['name'],
76
+ xml.attributes['type'],
77
+ xml.attributes['scope'],
78
+ xml.attributes['builtin'] == '1',
79
+ description)
80
+ end
81
+ end
82
+
83
+ # Definition object for a report template.
84
+ #
85
+ class ReportTemplate
86
+
87
+ # The ID of the report template.
88
+ attr_accessor :id
89
+ # The name of the report template.
90
+ attr_accessor :name
91
+ # With a data template, you can export comma-separated value (CSV) files
92
+ # with vulnerability-based data. With a document template, you can create
93
+ # PDF, RTF, HTML, or XML reports with asset-based information. When you
94
+ # retrieve a report template, the type will always be visible even though
95
+ # type is implied. When ReportTemplate is sent as a request, and the type
96
+ # attribute is not provided, the type attribute defaults to document,
97
+ # allowing for backward compatibility with existing API clients.
98
+ attr_accessor :type
99
+ # The visibility (scope) of the report template.
100
+ # One of: global|silo
101
+ attr_accessor :scope
102
+ # The report template is built-in, and cannot be modified.
103
+ attr_accessor :built_in
104
+ # Description of this report template.
105
+ attr_accessor :description
106
+
107
+ # Array of report sections.
108
+ attr_accessor :sections
109
+ # Map of report properties.
110
+ attr_accessor :properties
111
+ # Array of report attributes, in the order they will be present in a report.
112
+ attr_accessor :attributes
113
+ # Display asset names with IPs.
114
+ attr_accessor :show_device_names
115
+
116
+ def initialize(name, type = 'document', id = -1, scope = 'silo', built_in = false)
117
+ @name = name
118
+ @type = type
119
+ @id = id
120
+ @scope = scope
121
+ @built_in = built_in
122
+
123
+ @sections = []
124
+ @properties = {}
125
+ @attributes = []
126
+ @show_device_names = false
127
+ end
128
+
129
+ # Save the configuration for a report template.
130
+ def save(connection)
131
+ xml = %(<ReportTemplateSaveRequest session-id='#{connection.session_id}' scope='#{@scope}'>)
132
+ xml << to_xml
133
+ xml << '</ReportTemplateSaveRequest>'
134
+ response = connection.execute(xml)
135
+ if response.success
136
+ @id = response.attributes['template-id']
137
+ end
138
+ end
139
+
140
+ # Retrieve the configuration for a report template.
141
+ def self.load(connection, template_id)
142
+ xml = %(<ReportTemplateConfigRequest session-id='#{connection.session_id}' template-id='#{template_id}'/>)
143
+ ReportTemplate.parse(connection.execute(xml))
144
+ end
145
+
146
+ def delete(connection)
147
+ connection.delete_report_template(@id)
148
+ end
149
+
150
+ include Sanitize
151
+
152
+ def to_xml
153
+ xml = %(<ReportTemplate id='#{@id}' name='#{@name}' type='#{@type}')
154
+ xml << %( scope='#{@scope}') if @scope
155
+ xml << %( builtin='#{@built_in}') if @built_in
156
+ xml << '>'
157
+ xml << %(<description>#{@description}</description>) if @description
158
+
159
+ unless @attributes.empty?
160
+ xml << '<ReportAttributes>'
161
+ @attributes.each do |attr|
162
+ xml << %(<ReportAttribute name='#{attr}'/>)
163
+ end
164
+ xml << '</ReportAttributes>'
165
+ end
166
+
167
+ unless @sections.empty?
168
+ xml << '<ReportSections>'
169
+ properties.each_pair do |name, value|
170
+ xml << %(<property name='#{name}'>#{replace_entities(value)}</property>)
171
+ end
172
+ @sections.each { |section| xml << section.to_xml }
173
+ xml << '</ReportSections>'
174
+ end
175
+
176
+ xml << %(<Settings><showDeviceNames enabled='#{@show_device_names ? 1 : 0}' /></Settings>)
177
+ xml << '</ReportTemplate>'
178
+ end
179
+
180
+ def self.parse(xml)
181
+ xml.res.elements.each('//ReportTemplate') do |tmp|
182
+ template = ReportTemplate.new(tmp.attributes['name'],
183
+ tmp.attributes['type'],
184
+ tmp.attributes['id'],
185
+ tmp.attributes['scope'] || 'silo',
186
+ tmp.attributes['builtin'])
187
+ tmp.elements.each('//description') do |desc|
188
+ template.description = desc.text
189
+ end
190
+
191
+ tmp.elements.each('//ReportAttributes/ReportAttribute') do |attr|
192
+ template.attributes << attr.attributes['name']
193
+ end
194
+
195
+ tmp.elements.each('//ReportSections/property') do |property|
196
+ template.properties[property.attributes['name']] = property.text
197
+ end
198
+
199
+ tmp.elements.each('//ReportSection') do |section|
200
+ template.sections << Section.parse(section)
201
+ end
202
+
203
+ tmp.elements.each('//showDeviceNames') do |show|
204
+ template.show_device_names = show.attributes['enabled'] == '1'
205
+ end
206
+
207
+ return template
208
+ end
209
+ nil
210
+ end
211
+ end
212
+
213
+ # Section specific content to include in a report template.
214
+ #
215
+ class Section
216
+
217
+ # Name of the report section.
218
+ attr_accessor :name
219
+ # Map of properties specific to the report section.
220
+ attr_accessor :properties
221
+
222
+ def initialize(name)
223
+ @name = name
224
+ @properties = {}
225
+ end
226
+
227
+ include Sanitize
228
+
229
+ def to_xml
230
+ xml = %(<ReportSection name='#{@name}'>)
231
+ properties.each_pair do |name, value|
232
+ xml << %(<property name='#{name}'>#{replace_entities(value)}</property>)
233
+ end
234
+ xml << '</ReportSection>'
235
+ end
236
+
237
+ def self.parse(xml)
238
+ name = xml.attributes['name']
239
+ xml.elements.each("//ReportSection[@name='#{name}']") do |elem|
240
+ section = Section.new(name)
241
+ elem.elements.each("//ReportSection[@name='#{name}']/property") do |property|
242
+ section.properties[property.attributes['name']] = property.text
243
+ end
244
+ return section
245
+ end
246
+ nil
247
+ end
248
+ end
249
+ end
data/lib/nexpose/scan.rb CHANGED
@@ -2,12 +2,141 @@ module Nexpose
2
2
  module NexposeAPI
3
3
  include XMLUtils
4
4
 
5
+ # Perform an ad hoc scan of a single device.
6
+ #
7
+ # @param [Device] device Device to scan.
8
+ # @return [Scan] Scan launch information.
9
+ #
10
+ def scan_device(device)
11
+ scan_devices([device])
12
+ end
13
+
14
+ # Perform an ad hoc scan of a subset of devices for a site.
15
+ # Nexpose only allows devices from a single site to be submitted per
16
+ # request.
17
+ # Method is designed to take objects from a Device listing.
18
+ #
19
+ # For example:
20
+ # devices = nsc.devices(5)
21
+ # nsc.scan_devices(devices.take(10))
22
+ #
23
+ # @param [Array[Device]] devices List of devices to scan.
24
+ # @return [Scan] Scan launch information.
25
+ #
26
+ def scan_devices(devices)
27
+ site_id = devices.map { |d| d.site_id }.uniq.first
28
+ xml = make_xml('SiteDevicesScanRequest', { 'site-id' => site_id })
29
+ elem = REXML::Element.new('Devices')
30
+ devices.each do |device|
31
+ elem.add_element('device', { 'id' => "#{device.id}" })
32
+ end
33
+ xml.add_element(elem)
34
+
35
+ _scan_ad_hoc(xml)
36
+ end
37
+
38
+ # Perform an ad hoc scan of a single asset of a site.
39
+ #
40
+ # @param [Fixnum] site_id Site ID that the assets belong to.
41
+ # @param [HostName|IPRange] asset Asset to scan.
42
+ # @return [Scan] Scan launch information.
43
+ #
44
+ def scan_asset(site_id, asset)
45
+ scan_assets(site_id, [asset])
46
+ end
47
+
48
+ # Perform an ad hoc scan of a subset of assets for a site.
49
+ # Only assets from a single site should be submitted per request.
50
+ # Method is designed to take objects filtered from Site#assets.
51
+ #
52
+ # For example:
53
+ # site = Site.load(nsc, 5)
54
+ # nsc.scan_assets(5, site.assets.take(10))
55
+ #
56
+ # @param [Fixnum] site_id Site ID that the assets belong to.
57
+ # @param [Array[HostName|IPRange]] assets List of assets to scan.
58
+ # @return [Scan] Scan launch information.
59
+ #
60
+ def scan_assets(site_id, assets)
61
+ xml = make_xml('SiteDevicesScanRequest', { 'site-id' => site_id })
62
+ hosts = REXML::Element.new('Hosts')
63
+ assets.each { |asset| _append_asset!(hosts, asset) }
64
+ xml.add_element(hosts)
65
+
66
+ _scan_ad_hoc(xml)
67
+ end
68
+
69
+ # Perform an ad hoc scan of a subset of IP addresses for a site.
70
+ # Only IPs from a single site can be submitted per request,
71
+ # and IP addresses must already be included in the site configuration.
72
+ # Method is designed for scanning when the targets are coming from an
73
+ # external source that does not have access to internal identfiers.
74
+ #
75
+ # For example:
76
+ # to_scan = ['192.168.2.1', '192.168.2.107']
77
+ # nsc.scan_ips(5, to_scan)
78
+ #
79
+ # @param [Fixnum] site_id Site ID that the assets belong to.
80
+ # @param [Array[String]] ip_addresses Array of IP addresses to scan.
81
+ # @return [Scan] Scan launch information.
82
+ #
83
+ def scan_ips(site_id, ip_addresses)
84
+ xml = make_xml('SiteDevicesScanRequest', { 'site-id' => site_id })
85
+ hosts = REXML::Element.new('Hosts')
86
+ ip_addresses.each do |ip|
87
+ xml.add_element('range', { 'from' => ip })
88
+ end
89
+ xml.add_element(hosts)
90
+
91
+ _scan_ad_hoc(xml)
92
+ end
93
+
94
+ # Initiate a site scan.
95
+ #
96
+ # @param [Fixnum] site_id Site ID to scan.
97
+ # @return [Scan] Scan launch information.
98
+ #
99
+ def scan_site(site_id)
100
+ xml = make_xml('SiteScanRequest', { 'site-id' => site_id })
101
+ response = execute(xml)
102
+ Scan.parse(response.res) if response.success
103
+ end
104
+
105
+ # Utility method for appending a HostName or IPRange object into an
106
+ # XML object, in preparation for ad hoc scanning.
107
+ #
108
+ # @param [REXML::Document] xml Prepared API call to execute.
109
+ # @param [HostName|IPRange] asset Asset to append to XML.
110
+ #
111
+ def _append_asset!(xml, asset)
112
+ if asset.kind_of? Nexpose::IPRange
113
+ xml.add_element('range', { 'from' => asset.from, 'to' => asset.to })
114
+ else # Assume HostName
115
+ host = REXML::Element.new('host')
116
+ host.text = asset.host
117
+ xml.add_element(host)
118
+ end
119
+ end
120
+
121
+ # Utility method for executing prepared XML and extracting Scan launch
122
+ # information.
123
+ #
124
+ # @param [REXML::Document] xml Prepared API call to execute.
125
+ # @return [Scan] Scan launch information.
126
+ #
127
+ def _scan_ad_hoc(xml)
128
+ r = execute(xml)
129
+ Scan.parse(r.res)
130
+ end
131
+
5
132
  # Stop a running or paused scan.
6
133
  #
7
134
  # @param [Fixnum] scan_id ID of the scan to stop.
8
- # @param [Fixnum] wait_sec Number of seconds to wait for status to be updated. Default: 0
9
- def scan_stop(scan_id, wait_sec = 0)
10
- r = execute(make_xml('ScanStopRequest', {'scan-id' => scan_id}))
135
+ # @param [Fixnum] wait_sec Number of seconds to wait for status to be
136
+ # updated.
137
+ #
138
+ def stop_scan(scan_id, wait_sec = 0)
139
+ r = execute(make_xml('ScanStopRequest', { 'scan-id' => scan_id }))
11
140
  if r.success
12
141
  so_far = 0
13
142
  while so_far < wait_sec
@@ -20,36 +149,36 @@ module Nexpose
20
149
  r.success
21
150
  end
22
151
 
23
- def scan_status(param)
24
- r = execute(make_xml('ScanStatusRequest', {'scan-id' => param}))
152
+ # Retrieve the status of a scan.
153
+ #
154
+ # @param [Fixnum] scan_id The scan ID.
155
+ # @return [String] Current status of the scan.
156
+ #
157
+ def scan_status(scan_id)
158
+ r = execute(make_xml('ScanStatusRequest', { 'scan-id' => scan_id }))
25
159
  r.success ? r.attributes['status'] : nil
26
160
  end
27
161
 
28
- #----------------------------------------------------------------
29
162
  # Resumes a scan.
30
163
  #
31
- # @param scan_id The scan ID.
32
- # @return Success(0|1) if it exists or null.
33
- #----------------------------------------------------------------
34
- def scan_resume(scan_id)
35
- r = execute(make_xml('ScanResumeRequest', {'scan-id' => scan_id}))
164
+ # @param [Fixnum] scan_id The scan ID.
165
+ #
166
+ def resume_scan(scan_id)
167
+ r = execute(make_xml('ScanResumeRequest', { 'scan-id' => scan_id }))
36
168
  r.success ? r.attributes['success'] : nil
37
169
  end
38
170
 
39
-
40
- #----------------------------------------------------------------
41
171
  # Pauses a scan.
42
172
  #
43
- # @param scan_id The scan ID.
44
- # @return Success(0|1) if it exists or null.
45
- #----------------------------------------------------------------
46
- def scan_pause(scan_id)
47
- r = execute(make_xml('ScanPauseRequest',{ 'scan-id' => scan_id}))
173
+ # @param [Fixnum] scan_id The scan ID.
174
+ #
175
+ def pause_scan(scan_id)
176
+ r = execute(make_xml('ScanPauseRequest', { 'scan-id' => scan_id }))
48
177
  r.success ? r.attributes['success'] : nil
49
178
  end
50
179
 
51
- # Retrieve a list of current scan activities across all Scan Engines managed
52
- # by Nexpose.
180
+ # Retrieve a list of current scan activities across all Scan Engines
181
+ # managed by Nexpose.
53
182
  #
54
183
  # @return [Array[ScanSummary]] Array of ScanSummary objects associated with
55
184
  # each active scan on the engines.
@@ -67,10 +196,11 @@ module Nexpose
67
196
 
68
197
  # Get scan statistics, including node and vulnerability breakdowns.
69
198
  #
199
+ # @param [Fixnum] scan_id Scan ID to retrieve statistics for.
70
200
  # @return [ScanSummary] ScanSummary object providing statistics for the scan.
71
201
  #
72
202
  def scan_statistics(scan_id)
73
- r = execute(make_xml('ScanStatisticsRequest', {'scan-id' => scan_id}))
203
+ r = execute(make_xml('ScanStatisticsRequest', { 'scan-id' => scan_id }))
74
204
  if r.success
75
205
  ScanSummary.parse(r.res.elements['//ScanSummary'])
76
206
  else
@@ -79,7 +209,6 @@ module Nexpose
79
209
  end
80
210
  end
81
211
 
82
- # === Description
83
212
  # Object that represents a summary of a scan.
84
213
  #
85
214
  class ScanSummary
@@ -129,28 +258,33 @@ module Nexpose
129
258
  start_time = nil
130
259
  unless xml.attributes['startTime'] == ''
131
260
  start_time = DateTime.parse(xml.attributes['startTime'].to_s).to_time
261
+ # Timestamp is UTC, but parsed as local time.
262
+ start_time -= start_time.gmt_offset
132
263
  end
133
264
 
134
265
  # End time is often not present, since reporting on running scans.
135
266
  end_time = nil
136
267
  if xml.attributes['endTime']
137
268
  end_time = DateTime.parse(xml.attributes['endTime'].to_s).to_time
269
+ # Timestamp is UTC, but parsed as local time.
270
+ end_time -= end_time.gmt_offset
138
271
  end
139
- return ScanSummary.new(xml.attributes['scan-id'].to_i,
140
- xml.attributes['site-id'].to_i,
141
- xml.attributes['engine-id'].to_i,
142
- xml.attributes['status'],
143
- start_time,
144
- end_time,
145
- msg,
146
- tasks,
147
- nodes,
148
- vulns)
272
+ ScanSummary.new(xml.attributes['scan-id'].to_i,
273
+ xml.attributes['site-id'].to_i,
274
+ xml.attributes['engine-id'].to_i,
275
+ xml.attributes['status'],
276
+ start_time,
277
+ end_time,
278
+ msg,
279
+ tasks,
280
+ nodes,
281
+ vulns)
149
282
  end
150
283
 
151
284
  # Value class to tracking task counts.
152
285
  #
153
286
  class Tasks
287
+
154
288
  attr_reader :pending, :active, :completed
155
289
 
156
290
  def initialize(pending, active, completed)
@@ -164,15 +298,16 @@ module Nexpose
164
298
  #
165
299
  def self.parse(rexml)
166
300
  return nil unless rexml
167
- return Tasks.new(rexml.attributes['pending'].to_i,
168
- rexml.attributes['active'].to_i,
169
- rexml.attributes['completed'].to_i)
301
+ Tasks.new(rexml.attributes['pending'].to_i,
302
+ rexml.attributes['active'].to_i,
303
+ rexml.attributes['completed'].to_i)
170
304
  end
171
305
  end
172
306
 
173
307
  # Value class for tracking node counts.
174
308
  #
175
309
  class Nodes
310
+
176
311
  attr_reader :live, :dead, :filtered, :unresolved, :other
177
312
 
178
313
  def initialize(live, dead, filtered, unresolved, other)
@@ -186,20 +321,21 @@ module Nexpose
186
321
  #
187
322
  def self.parse(rexml)
188
323
  return nil unless rexml
189
- return Nodes.new(rexml.attributes['live'].to_i,
190
- rexml.attributes['dead'].to_i,
191
- rexml.attributes['filtered'].to_i,
192
- rexml.attributes['unresolved'].to_i,
193
- rexml.attributes['other'].to_i)
324
+ Nodes.new(rexml.attributes['live'].to_i,
325
+ rexml.attributes['dead'].to_i,
326
+ rexml.attributes['filtered'].to_i,
327
+ rexml.attributes['unresolved'].to_i,
328
+ rexml.attributes['other'].to_i)
194
329
  end
195
330
  end
196
331
 
197
332
  # Value class for tracking vulnerability counts.
198
333
  #
199
334
  class Vulnerabilities
335
+
200
336
  attr_reader :vuln_exploit, :vuln_version, :vuln_potential,
201
- :not_vuln_exploit, :not_vuln_version,
202
- :error, :disabled, :other
337
+ :not_vuln_exploit, :not_vuln_version,
338
+ :error, :disabled, :other
203
339
 
204
340
  def initialize(vuln_exploit, vuln_version, vuln_potential,
205
341
  not_vuln_exploit, not_vuln_version,
@@ -246,6 +382,7 @@ module Nexpose
246
382
  # and vuln-potential.
247
383
  #
248
384
  class Status
385
+
249
386
  attr_reader :severities, :count
250
387
 
251
388
  def initialize(severity = nil, count = 0)
@@ -269,19 +406,24 @@ module Nexpose
269
406
  end
270
407
  end
271
408
 
272
- # TODO: review
273
- # <scanFilter scanStop='0' scanFailed='0' scanStart='1'/>
274
- # === Description
409
+ # Struct class for tracking scan launch information.
275
410
  #
276
- class ScanFilter
277
- attr_reader :scan_stop
278
- attr_reader :scan_failed
279
- attr_reader :scan_start
280
-
281
- def initialize(scan_stop, scan_failed, scan_start)
282
- @scan_stop = scan_stop
283
- @scan_failed = scan_failed
284
- @scan_start = scan_start
411
+ class Scan
412
+
413
+ # The scan ID when a scan is successfully launched.
414
+ attr_reader :id
415
+ # The engine the scan was dispatched to.
416
+ attr_reader :engine
417
+
418
+ def initialize(scan_id, engine_id)
419
+ @id, @engine = scan_id, engine_id
420
+ end
421
+
422
+ def self.parse(xml)
423
+ xml.elements.each('//Scan') do |scan|
424
+ return new(scan.attributes['scan-id'].to_i,
425
+ scan.attributes['engine-id'].to_i)
426
+ end
285
427
  end
286
428
  end
287
429
  end