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,103 @@
1
+ module Nexpose
2
+ module NexposeAPI
3
+
4
+ # List the scan templates currently configured on the console.
5
+ #
6
+ # @return [Array[String]] list of scan templates IDs.
7
+ #
8
+ def list_scan_templates
9
+ templates = JSON.parse(AJAX.get(self, '/data/scan/templates'))
10
+ templates['valueList']
11
+ end
12
+
13
+ alias_method :scan_templates, :list_scan_templates
14
+
15
+ # Delete a scan template from the console.
16
+ # Cannot be used to delete a built-in template.
17
+ #
18
+ # @param [String] id Unique identifier of an existing scan template.
19
+ #
20
+ def delete_scan_template(id)
21
+ AJAX.delete(self, "/data/scan/templates/#{URI.encode(id)}")
22
+ end
23
+ end
24
+
25
+ # Configuration object for a scan template.
26
+ # This class is only a partial representation of some of the features
27
+ # available for configuration.
28
+ #
29
+ class ScanTemplate
30
+
31
+ # Unique identifier of the scan template.
32
+ attr_accessor :id
33
+
34
+ attr_accessor :name
35
+ attr_accessor :description
36
+
37
+ # Whether to correlate reliable checks with regular checks.
38
+ attr_accessor :correlate
39
+
40
+ # Parsed XML of a scan template
41
+ attr_accessor :xml
42
+
43
+ def initialize(xml)
44
+ @xml = xml
45
+
46
+ root = REXML::XPath.first(xml, 'ScanTemplate')
47
+ @id = root.attributes['id']
48
+
49
+ desc = REXML::XPath.first(root, 'templateDescription')
50
+ @name = desc.attributes['title']
51
+ @description = desc.text.to_s
52
+
53
+ vuln_checks = REXML::XPath.first(root, 'VulnerabilityChecks')
54
+ @correlate = vuln_checks.attributes['correlate'] == '1'
55
+ end
56
+
57
+ # Save this scan template configuration to a Nexpose console.
58
+ #
59
+ def save(nsc)
60
+ root = REXML::XPath.first(@xml, 'ScanTemplate')
61
+ existing = root.attributes['id'] == @id
62
+ root.attributes['id'] = @id unless existing
63
+
64
+ desc = REXML::XPath.first(root, 'templateDescription')
65
+ desc.attributes['title'] = @name
66
+ desc.text = @description
67
+
68
+ vuln_checks = REXML::XPath.first(root, 'VulnerabilityChecks')
69
+ vuln_checks.attributes['correlate'] = (@correlate ? '1' : '0')
70
+
71
+ if existing
72
+ response = AJAX.put(nsc, "/data/scan/templates/#{URI.encode(id)}", xml)
73
+ else
74
+ response = JSON.parse(AJAX.post(nsc, '/data/scan/templates', xml))
75
+ @id = response['value']
76
+ end
77
+ end
78
+
79
+ # Load an existing scan template.
80
+ #
81
+ # @param [Connection] nsc API connection to a Nexpose console.
82
+ # @param [String] id Unique identifier of an existing scan template.
83
+ # @return [ScanTemplate] The requested scan template configuration.
84
+ #
85
+ def self.load(nsc, id)
86
+ response = JSON.parse(AJAX.get(nsc, "/data/scan/templates/#{URI.encode(id)}"))
87
+ new(REXML::Document.new(response['value']))
88
+ end
89
+
90
+ # Copy an existing scan template, changing the id and title.
91
+ #
92
+ # @param [Connection] nsc API connection to a Nexpose console.
93
+ # @param [String] id Unique identifier of an existing scan template.
94
+ # @return [ScanTemplate] A copy of the requested scan template configuration.
95
+ #
96
+ def self.copy(nsc, id)
97
+ dupe = load(nsc, id)
98
+ dupe.id = "#{dupe.id}-copy"
99
+ dupe.title = "#{dupe.title} Copy"
100
+ dupe
101
+ end
102
+ end
103
+ end
data/lib/nexpose/site.rb CHANGED
@@ -2,79 +2,15 @@ module Nexpose
2
2
  module NexposeAPI
3
3
  include XMLUtils
4
4
 
5
- # Retrieve a list of all of the assets in a site.
6
- #
7
- # If no site-id is specified, then return all of the assets
8
- # for the Nexpose console, grouped by site-id.
9
- #
10
- # @param [FixNum] site_id Site ID to request device listing for. Optional.
11
- # @return [Array[Device]] Array of devices associated with the site, or
12
- # all devices on the console if no site is provided.
13
- #
14
- def site_device_listing(site_id = nil)
15
- r = execute(make_xml('SiteDeviceListingRequest', {'site-id' => site_id}))
16
-
17
- devices = []
18
- if r.success
19
- r.res.elements.each('SiteDeviceListingResponse/SiteDevices') do |site|
20
- site_id = site.attributes['site-id'].to_i
21
- site.elements.each('device') do |device|
22
- devices << Device.new(device.attributes['id'].to_i,
23
- device.attributes['address'],
24
- site_id,
25
- device.attributes['riskfactor'].to_f,
26
- device.attributes['riskscore'].to_f)
27
- end
28
- end
29
- end
30
- devices
31
- end
32
-
33
- alias_method :assets, :site_device_listing
34
- alias_method :devices, :site_device_listing
35
- alias_method :list_devices, :site_device_listing
36
-
37
- # Find a Device by its address.
38
- #
39
- # This is a convenience method for finding a single device from a SiteDeviceListing.
40
- # If no site_id is provided, the first matching device will be returned when a device
41
- # occurs across multiple sites.
42
- #
43
- # @param [String] address Address of the device to find. Usually the IP address.
44
- # @param [FixNum] site_id Site ID to request scan history for.
45
- # @return [Device] The first matching Device with the provided address, if found.
46
- #
47
- def find_device_by_address(address, site_id = nil)
48
- r = execute(make_xml('SiteDeviceListingRequest', {'site-id' => site_id}))
49
- if r.success
50
- device = REXML::XPath.first(r.res, "SiteDeviceListingResponse/SiteDevices/device[@address='#{address}']")
51
- return Device.new(device.attributes['id'].to_i,
52
- device.attributes['address'],
53
- device.parent.attributes['site-id'],
54
- device.attributes['riskfactor'].to_f,
55
- device.attributes['riskscore'].to_f) if device
56
- end
57
- nil
58
- end
59
-
60
- # Delete the specified site and all associated scan data.
61
- #
62
- # @return Whether or not the delete request succeeded.
63
- #
64
- def site_delete(site_id)
65
- r = execute(make_xml('SiteDeleteRequest', {'site-id' => site_id}))
66
- r.success
67
- end
68
-
69
5
  # Retrieve a list of all sites the user is authorized to view or manage.
70
6
  #
71
7
  # @return [Array[SiteSummary]] Array of SiteSummary objects.
72
- #
73
- def site_listing
8
+ #
9
+ def list_sites
74
10
  r = execute(make_xml('SiteListingRequest'))
75
11
  arr = []
76
- if (r.success)
77
- r.res.elements.each("SiteListingResponse/SiteSummary") do |site|
12
+ if r.success
13
+ r.res.elements.each('SiteListingResponse/SiteSummary') do |site|
78
14
  arr << SiteSummary.new(site.attributes['id'].to_i,
79
15
  site.attributes['name'],
80
16
  site.attributes['description'],
@@ -85,8 +21,16 @@ module Nexpose
85
21
  arr
86
22
  end
87
23
 
88
- alias_method :list_sites, :site_listing
89
- alias_method :sites, :site_listing
24
+ alias_method :sites, :list_sites
25
+
26
+ # Delete the specified site and all associated scan data.
27
+ #
28
+ # @return Whether or not the delete request succeeded.
29
+ #
30
+ def delete_site(site_id)
31
+ r = execute(make_xml('SiteDeleteRequest', {'site-id' => site_id}))
32
+ r.success
33
+ end
90
34
 
91
35
  # Retrieve a list of all previous scans of the site.
92
36
  #
@@ -111,68 +55,11 @@ module Nexpose
111
55
  # Method will not return data on an active scan.
112
56
  #
113
57
  # @param [FixNum] site_id Site ID to find latest scan for.
58
+ # @return [ScanSummary] details of the last completed scan for a site.
114
59
  #
115
60
  def last_scan(site_id)
116
61
  site_scan_history(site_id).select { |scan| scan.end_time }.max_by { |scan| scan.end_time }
117
62
  end
118
-
119
- #-----------------------------------------------------------------------
120
- # Starts device specific site scanning.
121
- #
122
- # devices - An Array of device IDs
123
- # hosts - An Array of Hashes [o]=>{:range=>"from,to"} [1]=>{:host=>host}
124
- #-----------------------------------------------------------------------
125
- def site_device_scan_start(site_id, devices, hosts = nil)
126
-
127
- if hosts == nil and devices == nil
128
- raise ArgumentError.new('Both the device and host list is nil.')
129
- end
130
-
131
- xml = make_xml('SiteDevicesScanRequest', {'site-id' => site_id})
132
-
133
- if devices != nil
134
- inner_xml = REXML::Element.new 'Devices'
135
- for device_id in devices
136
- inner_xml.add_element 'device', {'id' => "#{device_id}"}
137
- end
138
- xml.add_element inner_xml
139
- end
140
-
141
- if hosts
142
- inner_xml = REXML::Element.new 'Hosts'
143
- hosts.each_index do |x|
144
- if hosts[x].key? :range
145
- from, to = hosts[x][:range].split(',')
146
- if to
147
- inner_xml.add_element 'range', {'to' => to, 'from' => from}
148
- else
149
- inner_xml.add_element 'range', {'from' => from}
150
- end
151
- end
152
- if hosts[x].key? :host
153
- host_element = REXML::Element.new 'host'
154
- host_element.text = "#{hosts[x][:host]}"
155
- inner_xml.add_element host_element
156
- end
157
- end
158
- xml.add_element inner_xml
159
- end
160
-
161
- r = execute xml
162
- if r.success
163
- r.res.elements.each('//Scan') do |scan_info|
164
- return {
165
- :scan_id => scan_info.attributes['scan-id'].to_i,
166
- :engine_id => scan_info.attributes['engine-id'].to_i
167
- }
168
- end
169
- else
170
- false
171
- end
172
- end
173
-
174
- alias_method :site_device_scan, :site_device_scan_start
175
- alias_method :adhoc_device_scan, :site_device_scan_start
176
63
  end
177
64
 
178
65
  # Configuration object representing a Nexpose site.
@@ -270,6 +157,14 @@ module Nexpose
270
157
  @assets << IPRange.new(ip)
271
158
  end
272
159
 
160
+ # Adds assets to this site by IP address range.
161
+ #
162
+ # @param [String] from Beginning IP address of a range.
163
+ # @param [String] to Ending IP address of a range.
164
+ def add_ip_range(from, to)
165
+ @assets << IPRange.new(from, to)
166
+ end
167
+
273
168
  # Adds an asset to this site, resolving whether an IP or hostname is
274
169
  # provided.
275
170
  #
@@ -277,12 +172,14 @@ module Nexpose
277
172
  #
278
173
  def add_asset(asset)
279
174
  begin
175
+ # If the asset registers as a valid IP, store as IP.
176
+ ip = IPAddr.new(asset)
280
177
  add_ip(asset)
281
178
  rescue ArgumentError => e
282
179
  if e.message == 'invalid address'
283
180
  add_host(asset)
284
181
  else
285
- raise "Unable to parse asset: '#{asset}'"
182
+ raise "Unable to parse asset: '#{asset}'. #{e.message}"
286
183
  end
287
184
  end
288
185
  end
@@ -292,16 +189,21 @@ module Nexpose
292
189
  # @param [Connection] connection Connection to console where site exists.
293
190
  # @param [Fixnum] id Site ID of an existing site.
294
191
  # @return [Site] Site configuration loaded from a Nexpose console.
192
+ #
295
193
  def self.load(connection, id)
296
- r = APIRequest.execute(connection.url, %Q(<SiteConfigRequest session-id="#{connection.session_id}" site-id="#{id}"/>))
194
+ r = APIRequest.execute(connection.url,
195
+ %(<SiteConfigRequest session-id="#{connection.session_id}" site-id="#{id}"/>))
297
196
  parse(r.res)
298
197
  end
299
198
 
300
199
  # Copy an existing configuration from a Nexpose instance.
200
+ # Returned object will reset the site ID and append "Copy" to the existing
201
+ # name.
301
202
  #
302
- # @param [Connection] connection Connection to console where scan will be launched.
203
+ # @param [Connection] connection Connection to the security console.
303
204
  # @param [Fixnum] id Site ID of an existing site.
304
205
  # @return [Site] Site configuration loaded from a Nexpose console.
206
+ #
305
207
  def self.copy(connection, id)
306
208
  site = self.load(connection, id)
307
209
  site.id = -1
@@ -313,19 +215,19 @@ module Nexpose
313
215
  #
314
216
  # @param [Connection] connection Connection to console where this site will be saved.
315
217
  # @return [Fixnum] Site ID assigned to this configuration, if successful.
218
+ #
316
219
  def save(connection)
317
220
  r = connection.execute('<SiteSaveRequest session-id="' + connection.session_id + '">' + to_xml + ' </SiteSaveRequest>')
318
- if r.success
319
- @id = r.attributes['site-id'].to_i
320
- end
221
+ @id = r.attributes['site-id'].to_i if r.success
321
222
  end
322
223
 
323
224
  # Delete this site from a Nexpose console.
324
225
  #
325
226
  # @param [Connection] connection Connection to console where this site will be saved.
326
227
  # @return [Boolean] Whether or not the site was successfully deleted.
228
+ #
327
229
  def delete(connection)
328
- r = connection.execute(%Q{<SiteDeleteRequest session-id="#{connection.session_id}" site-id="#{@id}"/>})
230
+ r = connection.execute(%(<SiteDeleteRequest session-id="#{connection.session_id}" site-id="#{@id}"/>))
329
231
  r.success
330
232
  end
331
233
 
@@ -333,32 +235,34 @@ module Nexpose
333
235
  #
334
236
  # @param [Connection] connection Connection to console where scan will be launched.
335
237
  # @param [String] sync_id Optional synchronization token.
336
- # @return [Fixnum, Fixnum] Scan ID and engine ID where the scan was launched.
238
+ # @return [Scan] Scan launch information.
239
+ #
337
240
  def scan(connection, sync_id = nil)
338
241
  xml = REXML::Element.new('SiteScanRequest')
339
- xml.add_attributes({'session-id' => connection.session_id,
340
- 'site-id' => @id,
341
- 'sync-id' => sync_id})
242
+ xml.add_attributes({ 'session-id' => connection.session_id,
243
+ 'site-id' => @id,
244
+ 'sync-id' => sync_id })
342
245
 
343
246
  response = connection.execute(xml)
344
- if response.success
345
- scan = REXML::XPath.first(response.res, '/SiteScanResponse/Scan/')
346
- [scan.attributes['scan-id'].to_i, scan.attributes['engine-id'].to_i]
347
- end
247
+ Scan.parse(response.res) if response.success
348
248
  end
349
249
 
250
+ include Sanitize
251
+
350
252
  # Generate an XML representation of this site configuration
253
+ #
351
254
  # @return [String] XML valid for submission as part of other requests.
255
+ #
352
256
  def to_xml
353
- xml = %Q(<Site id='#{id}' name='#{name}' description='#{description}' riskfactor='#{risk_factor}'>)
257
+ xml = %(<Site id='#{id}' name='#{replace_entities(name)}' description='#{description}' riskfactor='#{risk_factor}'>)
354
258
 
355
259
  xml << '<Hosts>'
356
- xml << assets.reduce('') { |acc, host| acc << host.to_xml }
260
+ xml << assets.reduce('') { |a, e| a << e.to_xml }
357
261
  xml << '</Hosts>'
358
262
 
359
263
  unless exclude.empty?
360
264
  xml << '<ExcludedHosts>'
361
- xml << exclude.reduce('') { |acc, host| acc << host.to_xml }
265
+ xml << exclude.reduce('') { |a, e| a << e.to_xml }
362
266
  xml << '</ExcludedHosts>'
363
267
  end
364
268
 
@@ -378,7 +282,7 @@ module Nexpose
378
282
  xml << '</Alerting>'
379
283
  end
380
284
 
381
- xml << %Q(<ScanConfig configID="#{@id}" name="#{@scan_template_name || @scan_template}" templateID="#{@scan_template}" configVersion="#{@config_version || 3}" engineID="#{@engine}">)
285
+ xml << %(<ScanConfig configID="#{@id}" name="#{@scan_template_name || @scan_template}" templateID="#{@scan_template}" configVersion="#{@config_version || 3}" engineID="#{@engine}">)
382
286
 
383
287
  xml << '<Schedules>'
384
288
  @schedules.each do |schedule|
@@ -394,6 +298,7 @@ module Nexpose
394
298
  # @param [REXML::Document] rexml XML document to parse.
395
299
  # @return [Site] Site object represented by the XML.
396
300
  # ## TODO What is returned on failure?
301
+ #
397
302
  def self.parse(rexml)
398
303
  rexml.elements.each('SiteConfigResponse/Site') do |s|
399
304
  site = Site.new(s.attributes['name'])
@@ -417,8 +322,8 @@ module Nexpose
417
322
  end
418
323
 
419
324
  s.elements.each('Credentials/adminCredentials') do |credconf|
420
- cred = AdminCredentials.new(true)
421
- cred.set_service(credconf.attributes['service'])
325
+ cred = Credential.new
326
+ cred.service = credconf.attributes['service']
422
327
  cred.set_blob(credconf.get_text)
423
328
  site.credentials << cred
424
329
  end
@@ -443,62 +348,6 @@ module Nexpose
443
348
  end
444
349
  end
445
350
 
446
- # === Description
447
- # Object that represents a listing of all of the sites available on an NSC.
448
- #
449
- # === Example
450
- # # Create a new Nexpose Connection on the default port and Login
451
- # nsc = Connection.new("10.1.40.10","nxadmin","password")
452
- # nsc->login;
453
- #
454
- # # Get Site Listing
455
- # sitelisting = SiteListing.new(nsc)
456
- #
457
- # # Enumerate through all of the SiteSummaries
458
- # sitelisting.sites.each do |sitesummary|
459
- # # Do some operation on each site
460
- # end
461
- #
462
- class SiteListing
463
-
464
- # The NSC Connection associated with this object
465
- attr_reader :connection
466
- # Array containing SiteSummary objects for each site in the connection
467
- attr_reader :sites
468
- # The number of sites
469
- attr_reader :site_count
470
-
471
- # Constructor
472
- # SiteListing (connection)
473
- def initialize(connection)
474
- @sites = []
475
-
476
- @connection = connection
477
-
478
- r = @connection.execute('<SiteListingRequest session-id="' + @connection.session_id.to_s + '"/>')
479
-
480
- if r.success
481
- parse(r.res)
482
- else
483
- raise APIError.new(r, 'Failed to get site listing.')
484
- end
485
- end
486
-
487
- def parse(r)
488
- r.elements.each('SiteListingResponse/SiteSummary') do |s|
489
- site_summary = SiteSummary.new(
490
- s.attributes['id'].to_s,
491
- s.attributes['name'],
492
- s.attributes['description'],
493
- s.attributes['riskfactor'].to_s
494
- )
495
- @sites.push(site_summary)
496
- end
497
- @site_count = @sites.length
498
- end
499
- end
500
-
501
- # === Description
502
351
  # Object that represents the summary of a Nexpose Site.
503
352
  #
504
353
  class SiteSummary
@@ -516,7 +365,7 @@ module Nexpose
516
365
 
517
366
  # Constructor
518
367
  # SiteSummary(id, name, description, riskfactor = 1)
519
- def initialize(id, name, description, risk_factor = 1.0, risk_score = 0.0)
368
+ def initialize(id, name, description = nil, risk_factor = 1.0, risk_score = 0.0)
520
369
  @id = id
521
370
  @name = name
522
371
  @description = description
@@ -525,33 +374,10 @@ module Nexpose
525
374
  end
526
375
  end
527
376
 
528
- # === Description
529
- # Object that represents a single device in a Nexpose security console.
530
- #
531
- class Device
532
-
533
- # A unique device ID (assigned automatically by the Nexpose console).
534
- attr_reader :id
535
- # IP Address or Hostname of this device.
536
- attr_reader :address
537
- # User assigned risk multiplier.
538
- attr_reader :risk_factor
539
- # Nexpose risk score.
540
- attr_reader :risk_score
541
- # Site ID that this device is associated with.
542
- attr_reader :site_id
543
-
544
- def initialize(id, address, site_id, risk_factor = 1.0, risk_score = 0.0)
545
- @id = id.to_i
546
- @address = address
547
- @site_id = site_id.to_i
548
- @risk_factor = risk_factor.to_f
549
- @risk_score = risk_score.to_f
550
- end
551
- end
552
-
553
377
  # Object that represents a hostname to be added to a site.
378
+ #
554
379
  class HostName
380
+
555
381
  # Named host (usually DNS or Netbios name).
556
382
  attr_accessor :host
557
383
 
@@ -584,9 +410,9 @@ module Nexpose
584
410
  end
585
411
  end
586
412
 
587
- # === Description
588
413
  # Object that represents a single IP address or an inclusive range of IP addresses.
589
414
  # If to is nil then the from field will be used to specify a single IP Address only.
415
+ #
590
416
  class IPRange
591
417
 
592
418
  # Start of range *Required
@@ -595,18 +421,46 @@ module Nexpose
595
421
  attr_accessor :to
596
422
 
597
423
  def initialize(from, to = nil)
598
- @from = IPAddr.new(from)
599
- @to = IPAddr.new(to) if to
424
+ @from = from
425
+ @to = to unless from == to
600
426
  end
601
427
 
602
428
  include Comparable
603
429
 
604
430
  def <=>(other)
605
- to_xml <=> other.to_xml
431
+ from = IPAddr.new(@from)
432
+ to = @to.nil? ? from : IPAddr.new(@to)
433
+ cf_from = IPAddr.new(other.from)
434
+ cf_to = IPAddr.new(other.to.nil? ? other.from : other.to)
435
+ if cf_to < from
436
+ 1
437
+ elsif to < cf_from
438
+ -1
439
+ else # Overlapping
440
+ 0
441
+ end
442
+ end
443
+
444
+ def ==(other)
445
+ eql?(other)
606
446
  end
607
447
 
608
448
  def eql?(other)
609
- to_xml == other.to_xml
449
+ @from == other.from && @to == other.to
450
+ end
451
+
452
+ def include?(single_ip)
453
+ from = IPAddr.new(@from)
454
+ to = @to.nil? ? from : IPAddr.new(@to)
455
+ other = IPAddr.new(single_ip)
456
+
457
+ if other < from
458
+ false
459
+ elsif to < other
460
+ false
461
+ else
462
+ true
463
+ end
610
464
  end
611
465
 
612
466
  def hash
@@ -615,7 +469,7 @@ module Nexpose
615
469
 
616
470
  def to_xml_elem
617
471
  xml = REXML::Element.new('range')
618
- xml.add_attributes({'from' => @from, 'to' => @to})
472
+ xml.add_attributes({ 'from' => @from, 'to' => @to })
619
473
  xml
620
474
  end
621
475