Module: Nexpose::NexposeAPI

Includes:
XMLUtils
Included in:
Connection
Defined in:
lib/nexpose/report.rb,
lib/nexpose/pool.rb,
lib/nexpose/role.rb,
lib/nexpose/scan.rb,
lib/nexpose/silo.rb,
lib/nexpose/site.rb,
lib/nexpose/user.rb,
lib/nexpose/vuln.rb,
lib/nexpose/group.rb,
lib/nexpose/device.rb,
lib/nexpose/engine.rb,
lib/nexpose/filter.rb,
lib/nexpose/manage.rb,
lib/nexpose/ticket.rb,
lib/nexpose/shared_cred.rb,
lib/nexpose/scan_template.rb,
lib/nexpose/vuln_exception.rb,
lib/nexpose/report_template.rb

Overview

NexposeAPI module is mixed into the Connection object, and all methods are expected to be called from there.

Instance Method Summary collapse

Methods included from XMLUtils

#make_xml, #parse_xml

Instance Method Details

#_append_asset!(xml, asset) ⇒ Object

Utility method for appending a HostName or IPRange object into an XML object, in preparation for ad hoc scanning.

Parameters:

  • xml (REXML::Document)

    Prepared API call to execute.

  • asset (HostName|IPRange)

    Asset to append to XML.



111
112
113
114
115
116
117
118
119
# File 'lib/nexpose/scan.rb', line 111

def _append_asset!(xml, asset)
  if asset.kind_of? Nexpose::IPRange
    xml.add_element('range', { 'from' => asset.from, 'to' => asset.to })
  else  # Assume HostName
    host = REXML::Element.new('host')
    host.text = asset.host
    xml.add_element(host)
  end
end

#_scan_ad_hoc(xml) ⇒ Scan

Utility method for executing prepared XML and extracting Scan launch information.

Parameters:

  • xml (REXML::Document)

    Prepared API call to execute.

Returns:

  • (Scan)

    Scan launch information.



127
128
129
130
# File 'lib/nexpose/scan.rb', line 127

def _scan_ad_hoc(xml)
  r = execute(xml)
  Scan.parse(r.res)
end

#console_command(cmd_string) ⇒ Object

Execute an arbitrary console command that is supplied as text via the supplied parameter. Console commands are documented in the administrator’s guide. If you use a command that is not listed in the administrator’s guide, the application will return the XMLResponse.



11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# File 'lib/nexpose/manage.rb', line 11

def console_command(cmd_string)
  xml = make_xml('ConsoleCommandRequest', {})
  cmd = REXML::Element.new('Command')
  cmd.text = cmd_string
  xml << cmd

  r = execute(xml)
  if r.success
    r.res.elements.each('//Output') do |out|
      return out.text.to_s
    end
  else
    false
  end
end

#create_multi_tenant_user(user_config, silo_configs) ⇒ Object


Creates a multi-tenant user

user_config - A map of the user data.

REQUIRED PARAMS user-id, authsrcid, user-name, full-name, enabled, superuser

OPTIONAL PARAMS email, password

silo_configs - An array of maps of silo specific data

REQUIRED PARAMS silo-id, role-name, all-groups, all-sites, default-silo

allowed_groups/allowed_sites - An array of ids




31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/nexpose/silo.rb', line 31

def create_multi_tenant_user(user_config, silo_configs)
  xml = make_xml('MultiTenantUserCreateRequest')
  mtu_config_xml = make_xml('MultiTenantUserConfig', user_config, '', false)

  # Add the silo access
  silo_xml = make_xml('SiloAccesses', {}, '', false)
  silo_configs.each do |silo_config|
    silo_config_xml = make_xml('SiloAccess', {}, '', false)
    silo_config.keys.each do |k|
      if k == 'allowed_sites'
        allowed_sites_xml = make_xml('AllowedSites', {}, '', false)
        silo_config['allowed_sites'].each do |allowed_site|
          allowed_sites_xml.add_element(make_xml('AllowedSite', {'id' => allowed_site}, '', false))
        end
        silo_config_xml.add_element(allowed_sites_xml)
      elsif k == 'allowed_groups'
        allowed_groups_xml = make_xml('AllowedGroups', {}, '', false)
        silo_config['allowed_groups'].each do |allowed_group|
          allowed_groups_xml.add_element(make_xml('AllowedGroup', {'id' => allowed_group}, '', false))
        end
        silo_config_xml.add_element(allowed_groups_xml)
      else
        silo_config_xml.attributes[k] = silo_config[k]
      end
    end
    silo_xml.add_element(silo_config_xml)
  end
  mtu_config_xml.add_element(silo_xml)
  xml.add_element(mtu_config_xml)
  r = execute(xml, '1.2')
  r.success
end

#create_silo(silo_config) ⇒ Object


Creates a silo

silo_config - A map of the silo creation data.

REQUIRED PARAMS id, name, silo-profile-id, max-assets, max-hosted-assets, max-users

OPTIONAL PARAMS description




238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
# File 'lib/nexpose/silo.rb', line 238

def create_silo silo_config
  xml = make_xml 'SiloCreateRequest'
  silo_config_xml = make_xml 'SiloConfig', {}, '', false

  # Add the attributes
  silo_config.keys.each do |key|
    if not 'merchant'.eql? key and not 'organization'.eql? key
      silo_config_xml.attributes[key] = silo_config[key]
    end
  end

  # Add Organization info
  if silo_config['organization']
    org_xml = make_xml 'Organization', {}, '', false
    silo_config['organization'].keys.each do |key|
      if not 'address'.eql? key
        org_xml.attributes[key] = silo_config['organization'][key]
      end
    end

    address_xml = make_xml 'Address', silo_config['organization']['address'], '', false
    org_xml.add_element address_xml
    silo_config_xml.add_element org_xml
  end

  # Add Merchant info
  if silo_config['merchant']
    merchant_xml = make_xml 'Merchant', {}, '', false

    silo_config['merchant'].keys.each do |key|
      if not 'dba'.eql? key and not 'other_industries'.eql? key and not 'qsa'.eql? key and not 'address'.eql? key
        merchant_xml.attributes[key] = silo_config['merchant'][key]
      end
    end

    # Add the merchant address
    merchant_address_xml = make_xml 'Address', silo_config['merchant']['address'], '', false
    merchant_xml.add_element merchant_address_xml

    #Now add the complex data types
    if silo_config['merchant']['dba']
      dba_xml = make_xml 'DBAs', {}, '', false
      silo_config['merchant']['dba'].each do |name|
        dba_xml.add_element make_xml('DBA', {'name' => name}, '', false)
      end
      merchant_xml.add_element dba_xml
    end

    if silo_config['merchant']['other_industries']
      ois_xml = make_xml 'OtherIndustries', {}, '', false
      silo_config['merchant']['other_industries'].each do |name|
        ois_xml.add_element make_xml('Industry', {'name' => name}, '', false)
      end
      merchant_xml.add_element ois_xml
    end

    if silo_config['merchant']['qsa']
      qsa_xml = make_xml 'QSA', {}, '', false
      silo_config['merchant']['qsa'].keys.each do |key|
        if not 'address'.eql? key
          qsa_xml.attributes[key] = silo_config['merchant']['qsa'][key]
        end
      end

      # Add the address for this QSA
      address_xml = make_xml 'Address', silo_config['merchant']['qsa']['address'], '', false

      qsa_xml.add_element address_xml
      merchant_xml.add_element qsa_xml
    end
    silo_config_xml.add_element merchant_xml
  end

  xml.add_element silo_config_xml
  r = execute xml, '1.2'
  r.success
end

#create_silo_profile(silo_profile_config, permissions) ⇒ Object


Creates a silo profile

silo_config - A map of the silo data.

REQUIRED PARAMS id, name, all‐licensed-modules, all‐global-engines, all-global-report-templates, all‐global-scan‐templates

OPTIONAL PARAMS description

permissions - A map of an array of maps of silo specific data

REQUIRED PARAMS silo-id, role-name, all-groups, all-sites, default-silo

allowed_groups/allowed_sites - An array of ids




124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/nexpose/silo.rb', line 124

def create_silo_profile silo_profile_config, permissions
  xml = make_xml 'SiloProfileCreateRequest'
  spc_xml = make_xml('SiloProfileConfig', silo_profile_config, '', false)

  # Add the permissions
  if permissions['global_report_templates']
    grt_xml = make_xml('GlobalReportTemplates', {}, '', false)
    permissions['global_report_templates'].each do |name|
      grt_xml.add_element make_xml('GlobalReportTemplate', {'name' => name}, '', false)
    end
    spc_xml.add_element grt_xml
  end

  if permissions['global_scan_engines']
    gse_xml = make_xml('GlobalScanEngines', {}, '', false)
    permissions['global_scan_engines'].each do |name|
      gse_xml.add_element make_xml('GlobalScanEngine', {'name' => name}, '', false)
    end
    spc_xml.add_element gse_xml
  end

  if permissions['global_scan_templates']
    gst_xml = make_xml('GlobalScanTemplates', {}, '', false)
    permissions['global_scan_templates'].each do |name|
      gst_xml.add_element make_xml('GlobalScanTemplate', {'name' => name}, '', false)
    end
    spc_xml.add_element gst_xml
  end

  if permissions['licensed_modules']
    lm_xml = make_xml('LicensedModules', {}, '', false)
    permissions['licensed_modules'].each do |name|
      lm_xml.add_element make_xml('LicensedModule', {'name' => name}, '', false)
    end
    spc_xml.add_element lm_xml
  end

  if permissions['restricted_report_formats']
    rrf_xml = make_xml('RestrictedReportFormats', {}, '', false)
    permissions['restricted_report_formats'].each do |name|
      rrf_xml.add_element make_xml('RestrictedReportFormat', {'name' => name}, '', false)
    end
    spc_xml.add_element rrf_xml
  end

  if permissions['restricted_report_sections']
    rrs_xml = make_xml('RestrictedReportSections', {}, '', false)
    permissions['restricted_report_sections'].each do |name|
      rrs_xml.add_element make_xml('RestrictedReportSection', {'name' => name}, '', false)
    end
    spc_xml.add_element rrs_xml
  end

  xml.add_element spc_xml
  r = execute xml, '1.2'
  r.success
end

#delete_asset_group(id) ⇒ Boolean

Delete an asset group and all associated data.

Parameters:

  • id (Fixnum)

    Asset group ID to delete.

Returns:

  • (Boolean)

    Whether group deletion succeeded.



11
12
13
14
# File 'lib/nexpose/group.rb', line 11

def delete_asset_group(id)
  r = execute(make_xml('AssetGroupDeleteRequest', {'group-id' => id}))
  r.success
end

#delete_device(device_id) ⇒ Object



82
83
84
85
# File 'lib/nexpose/device.rb', line 82

def delete_device(device_id)
  r = execute(make_xml('DeviceDeleteRequest', { 'device-id' => device_id }))
  r.success
end

#delete_engine(engine_id, scope = 'silo') ⇒ Boolean

Removes a scan engine from the list of available engines.

Parameters:

  • engine_id (Fixnum)

    Unique ID of an existing engine to remove.

  • scope (String) (defaults to: 'silo')

    Whether the engine is global or silo scoped.

Returns:

  • (Boolean)

    true if engine successfully deleted.



11
12
13
14
15
16
# File 'lib/nexpose/engine.rb', line 11

def delete_engine(engine_id, scope = 'silo')
  xml = make_xml('EngineDeleteRequest',
                 {'engine-id' => engine_id, 'scope' => scope})
  response = execute(xml, '1.2')
  response.success
end

#delete_mtu(user_name, user_id) ⇒ Object


Delete a multi-tenant user




95
96
97
98
99
100
# File 'lib/nexpose/silo.rb', line 95

def delete_mtu user_name, user_id
  using_user_name = (user_name and not user_name.empty?)
  xml = make_xml('MultiTenantUserDeleteRequest', (using_user_name ? {'user-name' => user_name} : {'user-id' => user_id}))
  r = execute xml, '1.2'
  r.success
end

#delete_report(report_id) ⇒ Object

Delete a previously generated report.

Parameters:

  • report_id (Fixnum)

    ID of individual report to delete.



68
69
70
71
# File 'lib/nexpose/report.rb', line 68

def delete_report(report_id)
  xml = make_xml('ReportDeleteRequest', { 'report-id' => report_id })
  execute(xml).success
end

#delete_report_config(report_config_id) ⇒ Object

Delete a previously generated report definition. Also deletes any reports generated from that configuration.

Parameters:

  • report_config_id (Fixnum)

    ID of the report configuration to remove.



78
79
80
81
# File 'lib/nexpose/report.rb', line 78

def delete_report_config(report_config_id)
  xml = make_xml('ReportDeleteRequest', { 'reportcfg-id' => report_config_id })
  execute(xml).success
end

#delete_report_template(template_id) ⇒ Object

Deletes an existing, custom report template. Cannot delete built-in templates.

Parameters:

  • template_id (String)

    Unique identifier of the report template to remove.



32
33
34
# File 'lib/nexpose/report_template.rb', line 32

def delete_report_template(template_id)
  AJAX.delete(self, "/data/report/templates/#{template_id}")
end

#delete_scan_template(id) ⇒ Object

Delete a scan template from the console. Cannot be used to delete a built-in template.

Parameters:

  • id (String)

    Unique identifier of an existing scan template.



20
21
22
# File 'lib/nexpose/scan_template.rb', line 20

def delete_scan_template(id)
  AJAX.delete(self, "/data/scan/templates/#{URI.encode(id)}")
end

#delete_shared_credential(id) ⇒ Object Also known as: delete_shared_cred



17
18
19
# File 'lib/nexpose/shared_cred.rb', line 17

def delete_shared_credential(id)
  AJAX.post(self, "/data/credential/shared/delete?credid=#{id}")
end

#delete_silo(name, id) ⇒ Object


Delete a silo




341
342
343
344
345
346
# File 'lib/nexpose/silo.rb', line 341

def delete_silo name, id
  using_name = (name and not name.empty?)
  xml = make_xml('SiloDeleteRequest', (using_name ? {'silo-name' => name} : {'silo-id' => id}))
  r = execute xml, '1.2'
  r.success
end

#delete_silo_profile(name, id) ⇒ Object


Delete a silo profile




216
217
218
219
220
221
# File 'lib/nexpose/silo.rb', line 216

def delete_silo_profile name, id
  using_name = (name and not name.empty?)
  xml = make_xml('SiloProfileDeleteRequest', (using_name ? {'name' => name} : {'silo-profile-id' => id}))
  r = execute xml, '1.2'
  r.success
end

#delete_site(site_id) ⇒ Object

Delete the specified site and all associated scan data.

Returns:

  • Whether or not the delete request succeeded.



30
31
32
33
# File 'lib/nexpose/site.rb', line 30

def delete_site(site_id)
  r = execute(make_xml('SiteDeleteRequest', {'site-id' => site_id}))
  r.success
end

#delete_ticket(ticket) ⇒ Boolean

Deletes a Nexpose ticket.

Parameters:

  • ticket (Fixnum)

    Unique ID of the ticket to delete.

Returns:

  • (Boolean)

    Whether or not the ticket deletion succeeded.



26
27
28
29
# File 'lib/nexpose/ticket.rb', line 26

def delete_ticket(ticket)
  # TODO: Take Ticket object, too, and pull out IDs.
  delete_tickets([ticket])
end

#delete_tickets(tickets) ⇒ Boolean

Deletes a Nexpose ticket.

Parameters:

  • tickets (Array[Fixnum])

    Array of unique IDs of tickets to delete.

Returns:

  • (Boolean)

    Whether or not the ticket deletions succeeded.



36
37
38
39
40
41
42
43
44
# File 'lib/nexpose/ticket.rb', line 36

def delete_tickets(tickets)
  # TODO: Take Ticket objects, too, and pull out IDs.
  xml = make_xml('TicketDeleteRequest')
  tickets.each do |id|
    xml.add_element('Ticket', { 'id' => id })
  end

  (execute xml, '1.2').success
end

#delete_user(user_id) ⇒ Boolean

Delete a user from the Nexpose console.

Parameters:

  • user_id (Fixnum)

    Unique ID for the user to delete.

Returns:

  • (Boolean)

    Whether or not the user deletion succeeded.



36
37
38
39
# File 'lib/nexpose/user.rb', line 36

def delete_user(user_id)
  response = execute(make_xml('UserDeleteRequest', { 'id' => user_id }))
  response.success
end

#delete_vuln_exception(id) ⇒ Boolean

Delete an existing vulnerability exception.

Parameters:

  • id (Fixnum)

    The ID of a vuln exception.

Returns:

  • (Boolean)

    Whether or not deletion was successful.



73
74
75
76
77
# File 'lib/nexpose/vuln_exception.rb', line 73

def delete_vuln_exception(id)
  xml = make_xml('VulnerabilityExceptionDeleteRequest',
                 { 'exception-id' => id })
  execute(xml, '1.2').success
end

#engine_activity(engine_id) ⇒ Array[ScanSummary]

Provide a list of current scan activities for a specific Scan Engine.

Returns:

  • (Array[ScanSummary])

    Array of ScanSummary objects associated with each active scan on the engine.



23
24
25
26
27
28
29
30
31
32
33
# File 'lib/nexpose/engine.rb', line 23

def engine_activity(engine_id)
  xml = make_xml('EngineActivityRequest', {'engine-id' => engine_id})
  r = execute(xml)
  arr = []
  if r.success
    r.res.elements.each('//ScanSummary') do |scan_event|
      arr << ScanSummary.parse(scan_event)
    end
  end
  arr
end

#filter(field, operator, value = '') ⇒ Array[Asset]

Perform an asset filter search that will locate assets matching the provided conditions.

For example, the following call will return assets with Java installed:

nsc.filter(Search::Field::SOFTWARE, Search::Operator::CONTAINS, 'java')

Parameters:

  • field (String)

    Constant from Search::Field

  • operator (String)

    Constant from Search::Operator

  • value (String) (defaults to: '')

    Search term or constant from Search::Value

Returns:

  • (Array[Asset])

    List of matching assets.



16
17
18
19
20
# File 'lib/nexpose/filter.rb', line 16

def filter(field, operator, value = '')
  criterion = Criterion.new(field, operator, value)
  criteria = Criteria.new(criterion)
  search(criteria)
end

#find_device_by_address(address, site_id = nil) ⇒ Device Also known as: find_asset_by_address

Find a Device by its address.

This is a convenience method for finding a single device from a SiteDeviceListing. If no site_id is provided, the first matching device will be returned when a device occurs across multiple sites.

Parameters:

  • address (String)

    Address of the device to find. Usually the IP address.

  • site_id (FixNum) (defaults to: nil)

    Site ID to restrict search to.

Returns:

  • (Device)

    The first matching Device with the provided address, if found.



16
17
18
19
20
21
22
23
24
25
26
27
# File 'lib/nexpose/device.rb', line 16

def find_device_by_address(address, site_id = nil)
  r = execute(make_xml('SiteDeviceListingRequest', { 'site-id' => site_id }))
  if r.success
    device = REXML::XPath.first(r.res, "SiteDeviceListingResponse/SiteDevices/device[@address='#{address}']")
    return Device.new(device.attributes['id'].to_i,
                      device.attributes['address'],
                      device.parent.attributes['site-id'],
                      device.attributes['riskfactor'].to_f,
                      device.attributes['riskscore'].to_f) if device
  end
  nil
end

#find_vuln_check(search_term, partial_words = true, all_words = true) ⇒ Array[VulnCheck]

Search for Vulnerability Checks.

Parameters:

  • search_term (String)

    Search terms to search for.

  • partial_words (Boolean) (defaults to: true)

    Allow partial word matches.

  • all_words (Boolean) (defaults to: true)

    All words must be present.

Returns:

  • (Array[VulnCheck])

    List of matching Vulnerability Checks.



55
56
57
58
59
60
61
62
63
# File 'lib/nexpose/vuln.rb', line 55

def find_vuln_check(search_term, partial_words = true, all_words = true)
  uri = "/ajax/vulnck_synopsis.txml?phrase=#{URI.encode(search_term)}"
  uri += '&wholeWords=1' unless partial_words
  uri += '&allWords=1' if all_words
  data = DataTable._get_dyn_table(self, uri)
  data.map do |vuln|
    VulnCheck.new(vuln)
  end
end

#generate_report(report_id, wait = false) ⇒ Object

Generate a new report using the specified report definition.



28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/nexpose/report.rb', line 28

def generate_report(report_id, wait = false)
  xml = make_xml('ReportGenerateRequest', { 'report-id' => report_id })
  response = execute(xml)
  if response.success
    response.res.elements.each('//ReportSummary') do |summary|
      summary = ReportSummary.parse(summary)
      # If not waiting or the report is finished, return now.
      return summary unless wait && summary.status == 'Started'
    end
  end
  so_far = 0
  while wait
    summary = last_report(report_id)
    return summary unless summary.status == 'Started'
    sleep 5
    so_far += 5
    if so_far % 60 == 0
      puts "Still waiting. Current status: #{summary.status}"
    end
  end
  nil
end

#get_user_id(user_name) ⇒ Object

Retrieve the User ID based upon the user’s login name.

Parameters:

  • user_name (String)

    User name to search for.



27
28
29
# File 'lib/nexpose/user.rb', line 27

def get_user_id(user_name)
  users.find { |user| user.name.eql? user_name }
end

#last_report(report_config_id) ⇒ Object

Get details of the last report generated with the specified report id.



59
60
61
62
# File 'lib/nexpose/report.rb', line 59

def last_report(report_config_id)
  history = report_history(report_config_id)
  history.sort { |a, b| b.generated_on <=> a.generated_on }.first
end

#last_scan(site_id) ⇒ ScanSummary

Retrieve the scan summary statistics for the latest completed scan on a site.

Method will not return data on an active scan.

Parameters:

  • site_id (FixNum)

    Site ID to find latest scan for.

Returns:

  • (ScanSummary)

    details of the last completed scan for a site.



60
61
62
# File 'lib/nexpose/site.rb', line 60

def last_scan(site_id)
  site_scan_history(site_id).select { |scan| scan.end_time }.max_by { |scan| scan.end_time }
end

#list_asset_groupsArray[AssetGroupSummary] Also known as: groups, asset_groups

Retrieve an array of all asset groups the user is authorized to view or manage.

Returns:



21
22
23
24
25
26
27
28
29
30
31
32
33
34
# File 'lib/nexpose/group.rb', line 21

def list_asset_groups
  r = execute(make_xml('AssetGroupListingRequest'))

  groups = []
  if r.success
    r.res.elements.each('AssetGroupListingResponse/AssetGroupSummary') do |group|
      groups << AssetGroupSummary.new(group.attributes['id'].to_i,
                                   group.attributes['name'],
                                   group.attributes['description'],
                                   group.attributes['riskscore'].to_f)
    end
  end
  groups
end

#list_device_vulns(dev_id) ⇒ Array[Vulnerability] Also known as: list_asset_vulns, asset_vulns, device_vulns

List the vulnerability findings for a given device ID.

Parameters:

  • dev_id (Fixnum)

    Unique identifier of a device (asset).

Returns:



69
70
71
72
73
74
75
76
# File 'lib/nexpose/device.rb', line 69

def list_device_vulns(dev_id)
  parameters = { 'devid' => dev_id,
                 'table-id' => 'vulnerability-listing' }
  json = DataTable._get_json_table(self,
                                   '/data/vulnerability/asset-vulnerabilities',
                                   parameters)
  json.map { |vuln| VulnFinding.new(vuln) }
end

#list_engine_poolsArray[EnginePoolSummary] Also known as: engine_pools

Retrieve a list of all Scan Engine Pools managed by the Security Console.

Returns:

  • (Array[EnginePoolSummary])

    Array of EnginePoolSummary objects associated with each engine associated with this security console.



10
11
12
13
14
15
16
17
18
19
20
21
# File 'lib/nexpose/pool.rb', line 10

def list_engine_pools
  response = execute(make_xml('EnginePoolListingRequest'), '1.2')
  arr = []
  if response.success
    response.res.elements.each('EnginePoolListingResponse/EnginePoolSummary') do |pool|
      arr << EnginePoolSummary.new(pool.attributes['id'],
                                   pool.attributes['name'],
                                   pool.attributes['scope'])
    end
  end
  arr
end

#list_enginesArray[EngineSummary] Also known as: engines

Retrieve a list of all Scan Engines managed by the Security Console.

Returns:

  • (Array[EngineSummary])

    Array of EngineSummary objects associated with each engine associated with this security console.



40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/nexpose/engine.rb', line 40

def list_engines
  response = execute(make_xml('EngineListingRequest'))
  arr = []
  if response.success
    response.res.elements.each('//EngineSummary') do |engine|
      arr << EngineSummary.new(engine.attributes['id'].to_i,
                               engine.attributes['name'],
                               engine.attributes['address'],
                               engine.attributes['port'].to_i,
                               engine.attributes['status'])
    end
  end
  arr
end

#list_mtuObject


Lists all the multi-tenant users and their attributes.




67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/nexpose/silo.rb', line 67

def list_mtu
  xml = make_xml('MultiTenantUserListingRequest')
  r = execute xml, '1.2'

  if r.success
    res = []
    r.res.elements.each("//MultiTenantUserSummary") do |mtu|
      res << {
        :id => mtu.attributes['id'],
        :full_name => mtu.attributes['full-name'],
        :user_name => mtu.attributes['user-name'],
        :email => mtu.attributes['email'],
        :super_user => mtu.attributes['superuser'],
        :enabled => mtu.attributes['enabled'],
        :auth_module => mtu.attributes['auth-module'],
        :silo_count => mtu.attributes['silo-count'],
        :locked => mtu.attributes['locked']
      }
    end
    res
  else
    false
  end
end

#list_report_templatesArray[ReportTemplateSummary] Also known as: report_templates

Provide a list of all report templates the user can access on the Security Console.

Returns:



14
15
16
17
18
19
20
21
22
23
# File 'lib/nexpose/report_template.rb', line 14

def list_report_templates
  r = execute(make_xml('ReportTemplateListingRequest', {}))
  templates = []
  if r.success
    r.res.elements.each('//ReportTemplateSummary') do |template|
      templates << ReportTemplateSummary.parse(template)
    end
  end
  templates
end

#list_reportsArray[ReportConfigSummary] Also known as: reports

Provide a listing of all report definitions the user can access on the Security Console.

Returns:



14
15
16
17
18
19
20
21
22
23
# File 'lib/nexpose/report.rb', line 14

def list_reports
  r = execute(make_xml('ReportListingRequest'))
  reports = []
  if r.success
    r.res.elements.each('//ReportConfigSummary') do |report|
      reports << ReportConfigSummary.parse(report)
    end
  end
  reports
end

#list_scan_templatesArray[String] Also known as: scan_templates

List the scan templates currently configured on the console.

Returns:

  • (Array[String])

    list of scan templates IDs.



8
9
10
11
# File 'lib/nexpose/scan_template.rb', line 8

def list_scan_templates
  templates = JSON.parse(AJAX.get(self, '/data/scan/templates'))
  templates['valueList']
end

#list_shared_credentialsObject Also known as: list_shared_creds, shared_credentials, shared_creds



5
6
7
8
9
10
11
# File 'lib/nexpose/shared_cred.rb', line 5

def list_shared_credentials
  creds = DataTable._get_json_table(self,
                               '/data/credential/shared/listing',
                               { 'sort' => -1,
                                 'table-id' => 'credential-listing' })
  creds.map { |c| SharedCredentialSummary.from_json(c) }
end

#list_silo_profilesObject


Lists all the silo profiles and their attributes.




185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
# File 'lib/nexpose/silo.rb', line 185

def list_silo_profiles
  xml = make_xml('SiloProfileListingRequest')
  r = execute xml, '1.2'

  if r.success
    res = []
    r.res.elements.each("//SiloProfileSummary") do |silo_profile|
      res << {
        :id => silo_profile.attributes['id'],
        :name => silo_profile.attributes['name'],
        :description => silo_profile.attributes['description'],
        :global_report_template_count => silo_profile.attributes['global-report-template-count'],
        :global_scan_engine_count => silo_profile.attributes['global-scan-engine-count'],
        :global_scan_template_count => silo_profile.attributes['global-scan-template-count'],
        :licensed_module_count => silo_profile.attributes['licensed-module-count'],
        :restricted_report_section_count => silo_profile.attributes['restricted-report-section-count'],
        :all_licensed_modules => silo_profile.attributes['all-licensed-modules'],
        :all_global_engines => silo_profile.attributes['all-global-engines'],
        :all_global_report_templates => silo_profile.attributes['all-global-report-templates'],
        :all_global_scan_templates => silo_profile.attributes['all-global-scan-templates']
      }
    end
    res
  else
    false
  end
end

#list_silosObject


Lists all the silos and their attributes.




319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
# File 'lib/nexpose/silo.rb', line 319

def list_silos
  xml = make_xml('SiloListingRequest')
  r = execute xml, '1.2'

  if r.success
    res = []
    r.res.elements.each("//SiloSummary") do |silo_profile|
      res << {
        :id => silo_profile.attributes['id'],
        :name => silo_profile.attributes['name'],
        :description => silo_profile.attributes['description']
      }
    end
    res
  else
    false
  end
end

#list_site_devices(site_id = nil) ⇒ Array[Device] Also known as: devices, list_devices, assets, list_assets

Retrieve a list of all of the assets in a site.

If no site-id is specified, then return all of the assets for the Nexpose console, grouped by site-id.

Parameters:

  • site_id (FixNum) (defaults to: nil)

    Site ID to request device listing for. Optional.

Returns:

  • (Array[Device])

    Array of devices associated with the site, or all devices on the console if no site is provided.



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/nexpose/device.rb', line 40

def list_site_devices(site_id = nil)
  r = execute(make_xml('SiteDeviceListingRequest', { 'site-id' => site_id }))

  devices = []
  if r.success
    r.res.elements.each('SiteDeviceListingResponse/SiteDevices') do |site|
      site_id = site.attributes['site-id'].to_i
      site.elements.each('device') do |device|
        devices << Device.new(device.attributes['id'].to_i,
                              device.attributes['address'],
                              site_id,
                              device.attributes['riskfactor'].to_f,
                              device.attributes['riskscore'].to_f)
      end
    end
  end
  devices
end

#list_sitesArray[SiteSummary] Also known as: sites

Retrieve a list of all sites the user is authorized to view or manage.

Returns:

  • (Array[SiteSummary])

    Array of SiteSummary objects.



9
10
11
12
13
14
15
16
17
18
19
20
21
22
# File 'lib/nexpose/site.rb', line 9

def list_sites
  r = execute(make_xml('SiteListingRequest'))
  arr = []
  if r.success
    r.res.elements.each('SiteListingResponse/SiteSummary') do |site|
      arr << SiteSummary.new(site.attributes['id'].to_i,
                             site.attributes['name'],
                             site.attributes['description'],
                             site.attributes['riskfactor'].to_f,
                             site.attributes['riskscore'].to_f)
    end
  end
  arr
end

#list_ticketsObject Also known as: tickets



6
7
8
9
10
11
12
13
14
15
16
17
# File 'lib/nexpose/ticket.rb', line 6

def list_tickets
  # TODO: Should take in filters as arguments.
  xml = make_xml('TicketListingRequest')
  r = execute(xml, '1.2')
  tickets = []
  if r.success
    r.res.elements.each('TicketListingResponse/TicketSummary') do |summary|
      tickets << TicketSummary.parse(summary)
    end
  end
  tickets
end

#list_usersArray[UserSummary] Also known as: users

Retrieve a list of all users configured on this console.

Returns:



10
11
12
13
14
15
16
17
18
19
# File 'lib/nexpose/user.rb', line 10

def list_users
  r = execute(make_xml('UserListingRequest'))
  arr = []
  if r.success
    r.res.elements.each('UserListingResponse/UserSummary') do |summary|
      arr << UserSummary.parse(summary)
    end
  end
  arr
end

#list_vuln_exceptions(status = nil, duration = nil) ⇒ Array[VulnException] Also known as: vuln_exceptions

Retrieve vulnerability exceptions.

Parameters:

  • status (String) (defaults to: nil)

    Filter exceptions by the current status. @see Nexpose::VulnException::Status

  • duration (String) (defaults to: nil)

    A time interval in the format “PnYnMnDTnHnMnS”.

Returns:

  • (Array[VulnException])

    List of matching vulnerability exceptions.



12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# File 'lib/nexpose/vuln_exception.rb', line 12

def list_vuln_exceptions(status = nil, duration = nil)
  option = {}
  option['status'] = status if status
  option['time-duration'] = duration if duration
  xml = make_xml('VulnerabilityExceptionListingRequest', option)
  response = execute(xml, '1.2')

  xs = []
  if response.success
    response.res.elements.each('//VulnerabilityException') do |ve|
      xs << VulnException.parse(ve)
    end
  end
  xs
end

#list_vulns(full = false) ⇒ Array[Vulnerability|VulnerabilitySummary] Also known as: vulns

Retrieve summary details of all vulnerabilities.

Parameters:

  • full (Boolean) (defaults to: false)

    Whether or not to gather the full summary. Without the flag, only id, title, and severity are returned. It can take twice a long to retrieve full summary information.

Returns:



12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# File 'lib/nexpose/vuln.rb', line 12

def list_vulns(full = false)
  xml = make_xml('VulnerabilityListingRequest')
  # TODO: Add a flag to do stream parsing of the XML to improve performance.
  response = execute(xml, '1.2')
  vulns = []
  if response.success
    response.res.elements.each('VulnerabilityListingResponse/VulnerabilitySummary') do |vuln|
      if full
        vulns << VulnerabilitySummary.parse(vuln)
      else
        vulns << Vulnerability.new(vuln.attributes['id'],
                                   vuln.attributes['title'],
                                   vuln.attributes['severity'].to_i)
      end
    end
  end
  vulns
end

#pause_scan(scan_id) ⇒ Object

Pauses a scan.

Parameters:

  • scan_id (Fixnum)

    The scan ID.



175
176
177
178
# File 'lib/nexpose/scan.rb', line 175

def pause_scan(scan_id)
  r = execute(make_xml('ScanPauseRequest', { 'scan-id' => scan_id }))
  r.success ? r.attributes['success'] : nil
end

#recall_vuln_exception(id) ⇒ Boolean

Recall a vulnerability exception. Recall is used by a submitter to undo an exception request that has not been approved yet.

You can only recall a vulnerability exception that has ‘Under Review’ status.

Parameters:

  • id (Fixnum)

    Unique identifier of the exception to resubmit.

Returns:

  • (Boolean)

    Whether or not the recall was accepted by the console.



62
63
64
65
66
# File 'lib/nexpose/vuln_exception.rb', line 62

def recall_vuln_exception(id)
  xml = make_xml('VulnerabilityExceptionRecallRequest',
                 { 'exception-id' => id })
  execute(xml, '1.2').success
end

#report_history(report_config_id) ⇒ Object

Provide a history of all reports generated with the specified report definition.



53
54
55
56
# File 'lib/nexpose/report.rb', line 53

def report_history(report_config_id)
  xml = make_xml('ReportHistoryRequest', { 'reportcfg-id' => report_config_id })
  ReportSummary.parse_all(execute(xml))
end

#restartObject

Restart the application.

There is no response to a RestartRequest. When the application shuts down as part of the restart process, it terminates any active connections. Therefore, the application cannot issue a response when it restarts.



59
60
61
# File 'lib/nexpose/manage.rb', line 59

def restart
  execute(make_xml('RestartRequest', {})).success
end

#resubmit_vuln_exception(id, comment, reason = nil) ⇒ Boolean

Resubmit a vulnerability exception request with a new comment and reason after an exception has been rejected.

You can only resubmit a request that has a “Rejected” status; if an exception is “Approved” or “Under Review” you will receive an error message stating that the exception request cannot be resubmitted.

Parameters:

  • id (Fixnum)

    Unique identifier of the exception to resubmit.

  • comment (String)

    Comment to justify the exception resubmission.

  • reason (String) (defaults to: nil)

    The reason for the exception status, if changing. @see Nexpose::VulnException::Reason

Returns:

  • (Boolean)

    Whether or not the resubmission was valid.



43
44
45
46
47
48
49
50
51
# File 'lib/nexpose/vuln_exception.rb', line 43

def resubmit_vuln_exception(id, comment, reason = nil)
  options = { 'exception-id' => id }
  options['reason'] = reason if reason
  xml = make_xml('VulnerabilityExceptionResubmitRequest', options)
  comment_xml = make_xml('comment', {}, comment, false)
  xml.add_element(comment_xml)
  r = execute(xml, '1.2')
  r.success
end

#resume_scan(scan_id) ⇒ Object

Resumes a scan.

Parameters:

  • scan_id (Fixnum)

    The scan ID.



166
167
168
169
# File 'lib/nexpose/scan.rb', line 166

def resume_scan(scan_id)
  r = execute(make_xml('ScanResumeRequest', { 'scan-id' => scan_id }))
  r.success ? r.attributes['success'] : nil
end

#role_delete(role, scope = Scope::SILO) ⇒ Object Also known as: delete_role



66
67
68
69
70
71
72
# File 'lib/nexpose/role.rb', line 66

def role_delete(role, scope = Scope::SILO)
  xml = %Q(<RoleDeleteRequest session-id="#{@session_id}">)
  xml << %Q(<Role name="#{role}" scope="#{scope}"/>)
  xml << '</RoleDeleteRequest>' 
  response = execute(xml, '1.2')
  response.success
end

#role_listingObject Also known as: roles

Returns a summary list of all roles.



52
53
54
55
56
57
58
59
60
61
62
# File 'lib/nexpose/role.rb', line 52

def role_listing
  xml = make_xml('RoleListingRequest')
  r = execute(xml, '1.2')
  roles = []
  if r.success
    r.res.elements.each('RoleListingResponse/RoleSummary') do |summary|
      roles << RoleSummary::parse(summary)
    end
  end
  roles
end

#scan_activityArray[ScanSummary]

Retrieve a list of current scan activities across all Scan Engines managed by Nexpose.

Returns:

  • (Array[ScanSummary])

    Array of ScanSummary objects associated with each active scan on the engines.



186
187
188
189
190
191
192
193
194
195
# File 'lib/nexpose/scan.rb', line 186

def scan_activity
  r = execute(make_xml('ScanActivityRequest'))
  res = []
  if r.success
    r.res.elements.each('//ScanSummary') do |scan|
      res << ScanSummary.parse(scan)
    end
  end
  res
end

#scan_asset(site_id, asset) ⇒ Scan

Perform an ad hoc scan of a single asset of a site.

Parameters:

  • site_id (Fixnum)

    Site ID that the assets belong to.

  • asset (HostName|IPRange)

    Asset to scan.

Returns:

  • (Scan)

    Scan launch information.



44
45
46
# File 'lib/nexpose/scan.rb', line 44

def scan_asset(site_id, asset)
  scan_assets(site_id, [asset])
end

#scan_assets(site_id, assets) ⇒ Scan

Perform an ad hoc scan of a subset of assets for a site. Only assets from a single site should be submitted per request. Method is designed to take objects filtered from Site#assets.

For example:

site = Site.load(nsc, 5)
nsc.scan_assets(5, site.assets.take(10))

Parameters:

  • site_id (Fixnum)

    Site ID that the assets belong to.

  • assets (Array[HostName|IPRange])

    List of assets to scan.

Returns:

  • (Scan)

    Scan launch information.



60
61
62
63
64
65
66
67
# File 'lib/nexpose/scan.rb', line 60

def scan_assets(site_id, assets)
  xml = make_xml('SiteDevicesScanRequest', { 'site-id' => site_id })
  hosts = REXML::Element.new('Hosts')
  assets.each { |asset| _append_asset!(hosts, asset) }
  xml.add_element(hosts)

  _scan_ad_hoc(xml)
end

#scan_device(device) ⇒ Scan

Perform an ad hoc scan of a single device.

Parameters:

  • device (Device)

    Device to scan.

Returns:

  • (Scan)

    Scan launch information.



10
11
12
# File 'lib/nexpose/scan.rb', line 10

def scan_device(device)
  scan_devices([device])
end

#scan_devices(devices) ⇒ Scan

Perform an ad hoc scan of a subset of devices for a site. Nexpose only allows devices from a single site to be submitted per request. Method is designed to take objects from a Device listing.

For example:

devices = nsc.devices(5)
nsc.scan_devices(devices.take(10))

Parameters:

  • devices (Array[Device])

    List of devices to scan.

Returns:

  • (Scan)

    Scan launch information.



26
27
28
29
30
31
32
33
34
35
36
# File 'lib/nexpose/scan.rb', line 26

def scan_devices(devices)
  site_id = devices.map { |d| d.site_id }.uniq.first
  xml = make_xml('SiteDevicesScanRequest', { 'site-id' => site_id })
  elem = REXML::Element.new('Devices')
  devices.each do |device|
    elem.add_element('device', { 'id' => "#{device.id}" })
  end
  xml.add_element(elem)

  _scan_ad_hoc(xml)
end

#scan_ips(site_id, ip_addresses) ⇒ Scan

Perform an ad hoc scan of a subset of IP addresses for a site. Only IPs from a single site can be submitted per request, and IP addresses must already be included in the site configuration. Method is designed for scanning when the targets are coming from an external source that does not have access to internal identfiers.

For example:

to_scan = ['192.168.2.1', '192.168.2.107']
nsc.scan_ips(5, to_scan)

Parameters:

  • site_id (Fixnum)

    Site ID that the assets belong to.

  • ip_addresses (Array[String])

    Array of IP addresses to scan.

Returns:

  • (Scan)

    Scan launch information.



83
84
85
86
87
88
89
90
91
92
# File 'lib/nexpose/scan.rb', line 83

def scan_ips(site_id, ip_addresses)
  xml = make_xml('SiteDevicesScanRequest', { 'site-id' => site_id })
  hosts = REXML::Element.new('Hosts')
  ip_addresses.each do |ip|
    xml.add_element('range', { 'from' => ip })
  end
  xml.add_element(hosts)

  _scan_ad_hoc(xml)
end

#scan_site(site_id) ⇒ Scan

Initiate a site scan.

Parameters:

  • site_id (Fixnum)

    Site ID to scan.

Returns:

  • (Scan)

    Scan launch information.



99
100
101
102
103
# File 'lib/nexpose/scan.rb', line 99

def scan_site(site_id)
  xml = make_xml('SiteScanRequest', { 'site-id' => site_id })
  response = execute(xml)
  Scan.parse(response.res) if response.success
end

#scan_statistics(scan_id) ⇒ ScanSummary

Get scan statistics, including node and vulnerability breakdowns.

Parameters:

  • scan_id (Fixnum)

    Scan ID to retrieve statistics for.

Returns:

  • (ScanSummary)

    ScanSummary object providing statistics for the scan.



202
203
204
205
206
207
208
209
# File 'lib/nexpose/scan.rb', line 202

def scan_statistics(scan_id)
  r = execute(make_xml('ScanStatisticsRequest', { 'scan-id' => scan_id }))
  if r.success
    ScanSummary.parse(r.res.elements['//ScanSummary'])
  else
    false
  end
end

#scan_status(scan_id) ⇒ String

Retrieve the status of a scan.

Parameters:

  • scan_id (Fixnum)

    The scan ID.

Returns:

  • (String)

    Current status of the scan. See Nexpose::Scan::Status.



157
158
159
160
# File 'lib/nexpose/scan.rb', line 157

def scan_status(scan_id)
  r = execute(make_xml('ScanStatusRequest', { 'scan-id' => scan_id }))
  r.success ? r.attributes['status'] : nil
end

#search(criteria) ⇒ Array[Asset]

Perform a search that will match the criteria provided.

For example, the following call will return assets with Java and .NET:

java_criterion = Criterion.new(Search::Field::SOFTWARE,
                               Search::Operator::CONTAINS,
                               'java')
dot_net_criterion = Criterion.new(Search::Field::SOFTWARE,
                                  Search::Operator::CONTAINS,
                                  '.net')
criteria = Criteria.new([java_criterion, dot_net_criterion])
results = nsc.search(criteria)

Parameters:

  • criteria (Criteria)

    Criteria search object.

Returns:

  • (Array[Asset])

    List of matching assets.



37
38
39
40
41
42
# File 'lib/nexpose/filter.rb', line 37

def search(criteria)
  results = DataTable._get_json_table(self,
                                      '/data/asset/filterAssets',
                                      criteria._to_payload)
  results.map { |a| Asset.new(a) }
end

#send_log(uri = 'https://support.rapid7.com') ⇒ Object

Output diagnostic information into log files, zip the files, and encrypt the archive with a PGP public key that is provided as a parameter for the API call. Then upload the archive using HTTPS to a URL that is specified as an API parameter.

Parameters:

  • url

    Upload server to send the support log package to.



70
71
72
73
74
75
76
77
78
79
80
# File 'lib/nexpose/manage.rb', line 70

def send_log(uri = 'https://support.rapid7.com')
  url = REXML::Element.new('URL')
  url.text = uri
  tpt = REXML::Element.new('Transport')
  tpt.add_attribute('protocol', 'https')
  tpt << url
  xml = make_xml('SendLogRequest')
  xml << tpt

  execute(xml).success
end

#site_scan_history(site_id) ⇒ Array[ScanSummary]

Retrieve a list of all previous scans of the site.

Parameters:

  • site_id (FixNum)

    Site ID to request scan history for.

Returns:

  • (Array[ScanSummary])

    Array of ScanSummary objects representing each scan run to date on the site provided.



41
42
43
44
45
46
47
48
49
50
# File 'lib/nexpose/site.rb', line 41

def site_scan_history(site_id)
  r = execute(make_xml('SiteScanHistoryRequest', {'site-id' => site_id}))
  scans = []
  if r.success
    r.res.elements.each('SiteScanHistoryResponse/ScanSummary') do |scan_event|
      scans << ScanSummary.parse(scan_event)
    end
  end
  scans
end

#start_updateObject

Induce the application to retrieve required updates and restart if necessary.



48
49
50
# File 'lib/nexpose/manage.rb', line 48

def start_update
  execute(make_xml('StartUpdateRequest', {})).success
end

#stop_scan(scan_id, wait_sec = 0) ⇒ Object

Stop a running or paused scan.

Parameters:

  • scan_id (Fixnum)

    ID of the scan to stop.

  • wait_sec (Fixnum) (defaults to: 0)

    Number of seconds to wait for status to be updated.



138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/nexpose/scan.rb', line 138

def stop_scan(scan_id, wait_sec = 0)
  r = execute(make_xml('ScanStopRequest', { 'scan-id' => scan_id }))
  if r.success
    so_far = 0
    while so_far < wait_sec
      status = scan_status(scan_id)
      return status if status == 'stopped'
      sleep 5
      so_far += 5
    end
  end
  r.success
end

#system_informationObject

Obtain system data, such as total RAM, free RAM, total disk space, free disk space, CPU speed, number of CPU cores, and other vital information.



31
32
33
34
35
36
37
38
39
40
41
42
43
# File 'lib/nexpose/manage.rb', line 31

def system_information
  r = execute(make_xml('SystemInformationRequest', {}))

  if r.success
    res = {}
    r.res.elements.each('//Statistic') do |stat|
      res[stat.attributes['name'].to_s] = stat.text.to_s
    end
    res
  else
    false
  end
end

#vuln_details(vuln_id) ⇒ VulnerabilityDetail

Retrieve details for a vulnerability.

Parameters:

  • vuln_id (String)

    Nexpose vulnerability ID, such as ‘windows-duqu-cve-2011-3402’.

Returns:



38
39
40
41
42
43
44
45
46
# File 'lib/nexpose/vuln.rb', line 38

def vuln_details(vuln_id)
  xml = make_xml('VulnerabilityDetailsRequest', { 'vuln-id' => vuln_id })
  response = execute(xml, '1.2')
  if response.success
    response.res.elements.each('VulnerabilityDetailsResponse/Vulnerability') do |vuln|
      return VulnerabilityDetail.parse(vuln)
    end
  end
end