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,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