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.
data/lib/nexpose/creds.rb CHANGED
@@ -5,13 +5,12 @@ module Nexpose
5
5
  # the credentials will be returned as a security blob and can only
6
6
  # be passed back as is during a Site Save operation. This object
7
7
  # can only be used to create a new set of credentials.
8
- class AdminCredentials
8
+ #
9
+ class Credential
9
10
  include XMLUtils
10
11
 
11
12
  # Security blob for an existing set of credentials
12
- attr_accessor :securityblob
13
- # Designates if this object contains user defined credentials or a security blob
14
- attr_accessor :isblob
13
+ attr_accessor :blob
15
14
  # The service for these credentials. Can be All.
16
15
  attr_accessor :service
17
16
  # The host for these credentials. Can be Any.
@@ -37,22 +36,6 @@ module Nexpose
37
36
  # The password to use when escalating privileges (optional)
38
37
  attr_accessor :priv_password
39
38
 
40
- def initialize(isblob = false)
41
- @isblob = isblob
42
- end
43
-
44
- # Sets the credentials information for this object.
45
- def set_credentials(service, host, port, userid, password, realm)
46
- @isblob = false
47
- @securityblob = nil
48
- @service = service
49
- @host = host
50
- @port = port
51
- @userid = userid
52
- @password = password
53
- @realm = realm
54
- end
55
-
56
39
  def self.for_service(service, user, password, realm = nil, host = nil, port = nil)
57
40
  cred = new
58
41
  cred.service = service
@@ -64,41 +47,13 @@ module Nexpose
64
47
  cred
65
48
  end
66
49
 
67
- # Sets privilege escalation credentials. Type should be either
68
- # sudo/su.
69
- def set_privilege_credentials(type, username, password)
50
+ # Sets privilege escalation credentials. Type should be either sudo/su.
51
+ def add_privilege_credentials(type, username, password)
70
52
  @priv_type = type
71
53
  @priv_username = username
72
54
  @priv_password = password
73
55
  end
74
56
 
75
- # The name of the service. Possible values are outlined in the
76
- # Nexpose API docs.
77
- def set_service(service)
78
- @service = service
79
- end
80
-
81
- def set_host(host)
82
- @host = host
83
- end
84
-
85
- # Credentials fetched from the API are encrypted into a
86
- # securityblob. If you want to use those credentials on a
87
- # different site, copy the blob into the credential.
88
- def set_blob(securityblob)
89
- @isblob = true
90
- @securityblob = securityblob
91
- end
92
-
93
- # Add Headers to credentials for httpheaders.
94
- def set_headers(headers)
95
- @headers = headers
96
- end
97
-
98
- def set_html_forms(html_forms)
99
- @html_forms = html_forms
100
- end
101
-
102
57
  def to_xml
103
58
  to_xml_elem.to_s
104
59
  end
@@ -117,8 +72,7 @@ module Nexpose
117
72
  attributes['privilegeelevationusername'] = @priv_username if @priv_username
118
73
  attributes['privilegeelevationpassword'] = @priv_password if @priv_password
119
74
 
120
- data = isblob ? securityblob : ''
121
- xml = make_xml('adminCredentials', attributes, data)
75
+ xml = make_xml('adminCredentials', attributes, blob)
122
76
  xml.add_element(@headers.to_xml_elem) if @headers
123
77
  xml.add_element(@html_forms.to_xml_elem) if @html_forms
124
78
  xml
@@ -140,8 +94,10 @@ module Nexpose
140
94
  end
141
95
 
142
96
  # Object that represents Header name-value pairs, associated with Web Session Authentication.
97
+ #
143
98
  class Header
144
99
  include XMLUtils
100
+
145
101
  # Name, one per Header
146
102
  attr_reader :name
147
103
  # Value, one per Header
@@ -163,8 +119,10 @@ module Nexpose
163
119
  end
164
120
 
165
121
  # Object that represents Headers, associated with Web Session Authentication.
122
+ #
166
123
  class Headers
167
124
  include XMLUtils
125
+
168
126
  # A regular expression used to match against the response to identify authentication failures.
169
127
  attr_reader :soft403
170
128
  # Base URL of the application for which the form authentication applies.
@@ -197,8 +155,10 @@ module Nexpose
197
155
  end
198
156
 
199
157
  # When using htmlform, this represents the login form information.
158
+ #
200
159
  class Field
201
160
  include XMLUtils
161
+
202
162
  # The name of the HTML field (form parameter).
203
163
  attr_reader :name
204
164
  # The value of the HTML field (form parameter).
@@ -234,8 +194,10 @@ module Nexpose
234
194
  end
235
195
 
236
196
  # When using htmlform, this represents the login form information.
197
+ #
237
198
  class HTMLForm
238
199
  include XMLUtils
200
+
239
201
  # The name of the form being submitted.
240
202
  attr_reader :name
241
203
  # The HTTP action (URL) through which to submit the login form.
@@ -271,15 +233,15 @@ module Nexpose
271
233
  fields.each() do |field|
272
234
  xml.add_element(field.to_xml_elem)
273
235
  end
274
-
275
236
  xml
276
237
  end
277
-
278
238
  end
279
239
 
280
240
  # When using htmlform, this represents the login form information.
241
+ #
281
242
  class HTMLForms
282
243
  include XMLUtils
244
+
283
245
  # The URL of the login page containing the login form.
284
246
  attr_reader :parentpage
285
247
  # A regular expression used to match against the response to identify
@@ -314,7 +276,6 @@ module Nexpose
314
276
  end
315
277
  xml
316
278
  end
317
-
318
279
  end
319
280
 
320
281
  # When using ssh-key, this represents the PEM-format keypair information.
@@ -0,0 +1,73 @@
1
+ module Nexpose
2
+
3
+ # Dynamic Asset Group object.
4
+ #
5
+ class DynamicAssetGroup
6
+
7
+ # Unique name of this group.
8
+ attr_accessor :name
9
+ # Search criteria that defines which assets this group will aggregate.
10
+ attr_accessor :criteria
11
+ # Unique identifier of this group.
12
+ attr_accessor :id
13
+ # Description of this asset group.
14
+ attr_accessor :description
15
+ # Array of user IDs who have permission to access this group.
16
+ attr_accessor :users
17
+
18
+ def initialize(name, criteria = nil, description = nil)
19
+ @name, @criteria, @description = name, criteria, description
20
+ @users = []
21
+ end
22
+
23
+ # Save this dynamic asset group to the Nexpose console.
24
+ # Warning, saving this object does not set the id. It must be retrieved
25
+ # independently.
26
+ #
27
+ # @param [Connection] nsc Connection to a security console.
28
+ # @return [Boolean] Whether the group was successfully saved.
29
+ #
30
+ def save(nsc)
31
+ # load includes admin users, but save will fail if they are included.
32
+ admins = nsc.users.select { |u| u.is_admin }.map { |u| u.id }
33
+ @users.reject! { |id| admins.member? id }
34
+ data = JSON.parse(AJAX.form_post(nsc,
35
+ '/data/assetGroup/saveAssetGroup',
36
+ to_map))
37
+ data['response'] == 'success.'
38
+ end
39
+
40
+ # Load in an existing Dynamic Asset Group configuration.
41
+ #
42
+ # @param [Connection] nsc Connection to a security console.
43
+ # @param [Fixnum] id Unique identifier of an existing group.
44
+ # @return [DynamicAssetGroup] Dynamic asset group configuration.
45
+ #
46
+ def self.load(nsc, id)
47
+ json = JSON.parse(AJAX.get(nsc, "/data/assetGroup/loadAssetGroup?entityid=#{id}"))
48
+ raise APIError.new(json, json['message']) if json['response'] =~ /failure/
49
+ raise ArgumentError.new('Not a dynamic asset group.') unless json['dynamic']
50
+ dag = new(json['name'], Criteria.parse(json['searchCriteria']), json['tag'])
51
+ dag.id = id
52
+ dag.users = json['users']
53
+ dag
54
+ end
55
+
56
+ def to_map
57
+ obj = { 'searchCriteria' => @criteria.to_map,
58
+ 'name' => @name,
59
+ 'tag' => @description.nil? ? '' : @description,
60
+ 'dynamic' => true,
61
+ 'users' => @users }
62
+ map = { 'entityDetails' => JSON.generate(obj) }
63
+ if @id
64
+ map['entityid'] = @id
65
+ map['mode'] = 'edit'
66
+ else
67
+ map['entityid'] = false
68
+ map['mode'] = false
69
+ end
70
+ map
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,134 @@
1
+ module Nexpose
2
+
3
+ # Data table functions which extract data from the Nexpose UI.
4
+ #
5
+ # The functions in this file are utility functions for accessing data in the
6
+ # same manner as the Nexpose UI. These functions are not designed for external
7
+ # use, but to aid exposing data through other methods in the gem.
8
+ #
9
+ module DataTable
10
+ module_function
11
+
12
+ # Helper method to get the YUI tables into a consumable Ruby object.
13
+ #
14
+ # @param [Connection] console API connection to a Nexpose console.
15
+ # @param [String] address Controller address relative to https://host:port
16
+ # @param [Hash] parameters Parameters that need to be sent to the controller
17
+ # @param [Integer] pagination size
18
+ # @param [Integer] number of records to return, gets all if not specified
19
+ # The following attributes need to be provided:
20
+ # 'sort' Column to sort by
21
+ # 'table-id' The ID of the table to get from this controller
22
+ # @return [Array[Hash]] An array of hashes representing the requested table.
23
+ #
24
+ # Example usage:
25
+ # DataTable._get_json_table(@console,
26
+ # '/data/asset/site',
27
+ # { 'sort' => 'assetName',
28
+ # 'table-id' => 'site-assets',
29
+ # 'siteID' => site_id })
30
+ #
31
+ def _get_json_table(console, address, parameters, page_size = 500, records = nil)
32
+ parameters['dir'] = 'DESC'
33
+ parameters['startIndex'] = -1
34
+ parameters['results'] = -1
35
+
36
+ post = AJAX.form_post(console, address, parameters)
37
+ data = JSON.parse(post)
38
+ total = records || data['totalRecords']
39
+ return [] if total == 0
40
+
41
+ rows = []
42
+ parameters['results'] = page_size
43
+ while rows.length < total
44
+ parameters['startIndex'] = rows.length
45
+
46
+ data = JSON.parse(AJAX.form_post(console, address, parameters))
47
+ rows.concat data['records']
48
+ end
49
+ rows
50
+ end
51
+
52
+ # Helper method to get a Dyntable into a consumable Ruby object.
53
+ #
54
+ # @param [Connection] console API connection to a Nexpose console.
55
+ # @param [String] address Tag address with parameters relative to
56
+ # https://host:port
57
+ # @return [Array[Hash]] array of hashes representing the requested table.
58
+ #
59
+ # Example usage:
60
+ # DataTable._get_dyn_table(@console, '/data/asset/os/dyntable.xml?tableID=OSSynopsisTable')
61
+ #
62
+ def _get_dyn_table(console, address, payload = nil)
63
+ if payload
64
+ response = AJAX.post(console, address, payload)
65
+ else
66
+ response = AJAX.get(console, address)
67
+ end
68
+ response = REXML::Document.new(response)
69
+
70
+ headers = _dyn_headers(response)
71
+ rows = _dyn_rows(response)
72
+ rows.map { |row| Hash[headers.zip(row)] }
73
+ end
74
+
75
+ # Parse headers out of a dyntable response.
76
+ def _dyn_headers(response)
77
+ headers = []
78
+ response.elements.each('DynTable/MetaData/Column') do |header|
79
+ headers << header.attributes['name']
80
+ end
81
+ headers
82
+ end
83
+
84
+ # Parse rows out of a dyntable into an array of values.
85
+ def _dyn_rows(response)
86
+ rows = []
87
+ response.elements.each('DynTable/Data/tr') do |row|
88
+ rows << _dyn_record(row)
89
+ end
90
+ rows
91
+ end
92
+
93
+ # Parse records out of the row of a dyntable.
94
+ def _dyn_record(row)
95
+ record = []
96
+ row.elements.each('td') do |value|
97
+ record << (value.text ? value.text.to_s : '')
98
+ end
99
+ record
100
+ end
101
+
102
+ # Clean up the 'type-safe' IDs returned by many table requests.
103
+ # This is a destructive operation, changing the values in the underlying
104
+ # hash.
105
+ #
106
+ # @param [Array[Hash]] arr Array of hashes representing a data table.
107
+ # @param [String] id Key value of a type-safe ID to clean up.
108
+ #
109
+ # Example usage:
110
+ # # For data like: {"assetID"=>{"ID"=>2818}, "assetIP"=>"10.4.16.1", ...}
111
+ # _clean_data_table!(data, 'assetID')
112
+ #
113
+ def _clean_data_table!(arr, id)
114
+ arr.reduce([]) do |acc, hash|
115
+ acc << _clean_id!(hash, id)
116
+ end
117
+ end
118
+
119
+ # Convert a type-safe ID into a regular ID inside a hash.
120
+ #
121
+ # @param [Hash] hash Hash map containing a type-safe ID as one key.
122
+ # @param [String] id Key value of a type-safe ID to clean up.
123
+ #
124
+ def _clean_id!(hash, id)
125
+ hash.each_pair do |key, value|
126
+ if key == id
127
+ hash[key] = value['ID']
128
+ else
129
+ hash[key] = value
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,111 @@
1
+ module Nexpose
2
+ module NexposeAPI
3
+ include XMLUtils
4
+
5
+ # Find a Device by its address.
6
+ #
7
+ # This is a convenience method for finding a single device from a SiteDeviceListing.
8
+ # If no site_id is provided, the first matching device will be returned when a device
9
+ # occurs across multiple sites.
10
+ #
11
+ # @param [String] address Address of the device to find. Usually the IP address.
12
+ # @param [FixNum] site_id Site ID to restrict search to.
13
+ # @return [Device] The first matching Device with the provided address,
14
+ # if found.
15
+ #
16
+ def find_device_by_address(address, site_id = nil)
17
+ r = execute(make_xml('SiteDeviceListingRequest', { 'site-id' => site_id }))
18
+ if r.success
19
+ device = REXML::XPath.first(r.res, "SiteDeviceListingResponse/SiteDevices/device[@address='#{address}']")
20
+ return Device.new(device.attributes['id'].to_i,
21
+ device.attributes['address'],
22
+ device.parent.attributes['site-id'],
23
+ device.attributes['riskfactor'].to_f,
24
+ device.attributes['riskscore'].to_f) if device
25
+ end
26
+ nil
27
+ end
28
+
29
+ alias_method :find_asset_by_address, :find_device_by_address
30
+
31
+ # Retrieve a list of all of the assets in a site.
32
+ #
33
+ # If no site-id is specified, then return all of the assets
34
+ # for the Nexpose console, grouped by site-id.
35
+ #
36
+ # @param [FixNum] site_id Site ID to request device listing for. Optional.
37
+ # @return [Array[Device]] Array of devices associated with the site, or
38
+ # all devices on the console if no site is provided.
39
+ #
40
+ def list_site_devices(site_id = nil)
41
+ r = execute(make_xml('SiteDeviceListingRequest', { 'site-id' => site_id }))
42
+
43
+ devices = []
44
+ if r.success
45
+ r.res.elements.each('SiteDeviceListingResponse/SiteDevices') do |site|
46
+ site_id = site.attributes['site-id'].to_i
47
+ site.elements.each('device') do |device|
48
+ devices << Device.new(device.attributes['id'].to_i,
49
+ device.attributes['address'],
50
+ site_id,
51
+ device.attributes['riskfactor'].to_f,
52
+ device.attributes['riskscore'].to_f)
53
+ end
54
+ end
55
+ end
56
+ devices
57
+ end
58
+
59
+ alias_method :devices, :list_site_devices
60
+ alias_method :list_devices, :list_site_devices
61
+ alias_method :assets, :list_site_devices
62
+ alias_method :list_assets, :list_site_devices
63
+
64
+ # List the vulnerability findings for a given device ID.
65
+ #
66
+ # @param [Fixnum] dev_id Unique identifier of a device (asset).
67
+ # @return [Array[Vulnerability]] List of vulnerability findings.
68
+ #
69
+ def list_device_vulns(dev_id)
70
+ parameters = { 'devid' => dev_id,
71
+ 'table-id' => 'vulnerability-listing' }
72
+ json = DataTable._get_json_table(self,
73
+ '/data/vulnerability/asset-vulnerabilities',
74
+ parameters)
75
+ json.map { |vuln| VulnFinding.new(vuln) }
76
+ end
77
+
78
+ alias_method :list_asset_vulns, :list_device_vulns
79
+ alias_method :asset_vulns, :list_device_vulns
80
+ alias_method :device_vulns, :list_device_vulns
81
+
82
+ def delete_device(device_id)
83
+ r = execute(make_xml('DeviceDeleteRequest', { 'device-id' => device_id }))
84
+ r.success
85
+ end
86
+ end
87
+
88
+ # Object that represents a single device in a Nexpose security console.
89
+ #
90
+ class Device
91
+
92
+ # A unique device ID (assigned automatically by the Nexpose console).
93
+ attr_reader :id
94
+ # IP Address or Hostname of this device.
95
+ attr_reader :address
96
+ # User assigned risk multiplier.
97
+ attr_reader :risk_factor
98
+ # Nexpose risk score.
99
+ attr_reader :risk_score
100
+ # Site ID that this device is associated with.
101
+ attr_reader :site_id
102
+
103
+ def initialize(id, address, site_id, risk_factor = 1.0, risk_score = 0.0)
104
+ @id = id.to_i
105
+ @address = address
106
+ @site_id = site_id.to_i
107
+ @risk_factor = risk_factor.to_f
108
+ @risk_score = risk_score.to_f
109
+ end
110
+ end
111
+ end