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.
- checksums.yaml +4 -4
- data/lib/nexpose.rb +18 -9
- data/lib/nexpose/ajax.rb +127 -0
- data/lib/nexpose/alert.rb +29 -36
- data/lib/nexpose/common.rb +13 -12
- data/lib/nexpose/connection.rb +18 -13
- data/lib/nexpose/creds.rb +16 -55
- data/lib/nexpose/dag.rb +73 -0
- data/lib/nexpose/data_table.rb +134 -0
- data/lib/nexpose/device.rb +111 -0
- data/lib/nexpose/engine.rb +194 -0
- data/lib/nexpose/filter.rb +341 -0
- data/lib/nexpose/group.rb +33 -37
- data/lib/nexpose/manage.rb +4 -0
- data/lib/nexpose/pool.rb +142 -0
- data/lib/nexpose/report.rb +72 -278
- data/lib/nexpose/report_template.rb +249 -0
- data/lib/nexpose/scan.rb +196 -54
- data/lib/nexpose/scan_template.rb +103 -0
- data/lib/nexpose/site.rb +91 -237
- data/lib/nexpose/ticket.rb +173 -119
- data/lib/nexpose/user.rb +11 -3
- data/lib/nexpose/vuln.rb +81 -338
- data/lib/nexpose/vuln_exception.rb +368 -0
- metadata +12 -4
- data/lib/nexpose/misc.rb +0 -35
- data/lib/nexpose/scan_engine.rb +0 -325
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
|
-
|
8
|
+
#
|
9
|
+
class Credential
|
9
10
|
include XMLUtils
|
10
11
|
|
11
12
|
# Security blob for an existing set of credentials
|
12
|
-
attr_accessor :
|
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.
|
68
|
-
|
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
|
-
|
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.
|
data/lib/nexpose/dag.rb
ADDED
@@ -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
|