nexpose 0.2.8 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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