nexpose 0.2.8 → 0.5.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,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