nexpose 0.1.14 → 0.2.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/common.rb +8 -0
- data/lib/nexpose/group.rb +18 -17
- data/lib/nexpose/role.rb +242 -10
- data/lib/nexpose/scan.rb +13 -13
- data/lib/nexpose/site.rb +10 -10
- data/lib/nexpose/ticket.rb +110 -16
- data/lib/nexpose/vuln.rb +166 -179
- metadata +4 -4
data/lib/nexpose/common.rb
CHANGED
data/lib/nexpose/group.rb
CHANGED
@@ -25,7 +25,7 @@ module Nexpose
|
|
25
25
|
|
26
26
|
res = []
|
27
27
|
if r.success
|
28
|
-
r.res.elements.each('
|
28
|
+
r.res.elements.each('AssetGroupListingResponse/AssetGroupSummary') do |group|
|
29
29
|
res << AssetGroupSummary.new(group.attributes['id'].to_i,
|
30
30
|
group.attributes['name'].to_s,
|
31
31
|
group.attributes['description'].to_s,
|
@@ -36,6 +36,7 @@ module Nexpose
|
|
36
36
|
end
|
37
37
|
|
38
38
|
alias_method :asset_groups_listing, :asset_groups
|
39
|
+
alias_method :groups, :asset_groups
|
39
40
|
end
|
40
41
|
|
41
42
|
# Summary value object for asset group information.
|
@@ -127,23 +128,23 @@ module Nexpose
|
|
127
128
|
|
128
129
|
alias_method :get, :load
|
129
130
|
|
130
|
-
def self.parse(
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
return asset_group
|
131
|
+
def self.parse(xml)
|
132
|
+
puts xml
|
133
|
+
return nil unless xml
|
134
|
+
|
135
|
+
group = REXML::XPath.first(xml, 'AssetGroupConfigResponse/AssetGroup')
|
136
|
+
asset_group = new(group.attributes['name'].to_s,
|
137
|
+
group.attributes['description'].to_s,
|
138
|
+
group.attributes['id'].to_i,
|
139
|
+
group.attributes['riskscore'].to_f)
|
140
|
+
group.elements.each('Devices/device') do |dev|
|
141
|
+
asset_group.devices << Device.new(dev.attributes['id'].to_i,
|
142
|
+
dev.attributes['address'].to_s,
|
143
|
+
dev.attributes['site-id'].to_i,
|
144
|
+
dev.attributes['riskfactor'].to_f,
|
145
|
+
dev.attributes['riskscore'].to_f)
|
146
146
|
end
|
147
|
+
asset_group
|
147
148
|
end
|
148
149
|
end
|
149
150
|
end
|
data/lib/nexpose/role.rb
CHANGED
@@ -1,27 +1,259 @@
|
|
1
1
|
module Nexpose
|
2
2
|
|
3
|
+
# Constants
|
4
|
+
module Privilege
|
5
|
+
|
6
|
+
module Global
|
7
|
+
CREATE_REPORTS = 'CreateReports'
|
8
|
+
CONFIGURE_GLOBAL_SETTINGS = 'ConfigureGlobalSettings'
|
9
|
+
MANAGE_SITES = 'ManageSites'
|
10
|
+
MANAGE_ASSET_GROUPS = 'ManageAssetGroups'
|
11
|
+
MANAGE_DYNAMIC_ASSET_GROUPS = 'ManageDynamicAssetGroups'
|
12
|
+
MANAGE_SCAN_TEMPLATES = 'ManageScanTemplates'
|
13
|
+
MANAGE_REPORT_TEMPLATES = 'ManageReportTemplates'
|
14
|
+
GENERATE_RESTRICTED_REPORTS = 'GenerateRestrictedReports'
|
15
|
+
MANAGE_SCAN_ENGINES = 'ManageScanEngines'
|
16
|
+
SUBMIT_VULN_EXCEPTIONS = 'SubmitVulnExceptions'
|
17
|
+
APPROVE_VULN_EXCEPTIONS = 'ApproveVulnExceptions'
|
18
|
+
DELETE_VULN_EXCEPTIONS = 'DeleteVulnExceptions'
|
19
|
+
CREATE_TICKETS = 'CreateTickets'
|
20
|
+
CLOSE_TICKETS = 'CloseTickets'
|
21
|
+
TICKET_ASSIGNEE = 'TicketAssignee'
|
22
|
+
ADD_USERS_TO_SITE = 'AddUsersToSite'
|
23
|
+
ADD_USERS_TO_GROUP = 'AddUsersToGroup'
|
24
|
+
ADD_USERS_TO_REPORT = 'AddUsersToReport'
|
25
|
+
MANAGE_POLICIES = 'ManagePolicies'
|
26
|
+
end
|
27
|
+
|
28
|
+
module Site
|
29
|
+
VIEW_ASSET_DATA = 'ViewAssetData' # NOTE Duplicated between Site and AssetGroup
|
30
|
+
CONFIGURE_ALERTS = 'ConfigureAlerts'
|
31
|
+
CONFIGURE_CREDENTIALS = 'ConfigureCredentials'
|
32
|
+
CONFIGURE_ENGINES = 'ConfigureEngines'
|
33
|
+
CONFIGURE_SCAN_TEMPLATES = 'ConfigureScanTemplates'
|
34
|
+
CONFIGURE_SCHEDULE_SCANS = 'ConfigureScheduleScans'
|
35
|
+
CONFIGURE_SITE_SETTINGS = 'ConfigureSiteSettings'
|
36
|
+
CONFIGURE_TARGETS = 'ConfigureTargets'
|
37
|
+
MANUAL_SCANS = 'ManualScans'
|
38
|
+
PURGE_DATA = 'PurgeData'
|
39
|
+
end
|
40
|
+
|
41
|
+
module AssetGroup
|
42
|
+
CONFIGURE_ASSETS = 'ConfigureAssets'
|
43
|
+
VIEW_ASSET_DATA = 'ViewAssetData' # NOTE Duplicated between Site and AssetGroup
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
3
47
|
module NexposeAPI
|
4
48
|
include XMLUtils
|
5
49
|
|
6
50
|
# Returns a summary list of all roles.
|
51
|
+
#
|
7
52
|
def role_listing
|
8
53
|
xml = make_xml('RoleListingRequest')
|
9
54
|
r = execute(xml, '1.2')
|
55
|
+
roles = []
|
10
56
|
if r.success
|
11
|
-
res = []
|
12
57
|
r.res.elements.each('RoleListingResponse/RoleSummary') do |summary|
|
13
|
-
|
14
|
-
:id => summary.attributes['id'],
|
15
|
-
:name => summary.attributes['name'],
|
16
|
-
:full_name => summary.attributes['full-name'],
|
17
|
-
:description => summary.attributes['description'],
|
18
|
-
:enabled => summary.attributes['enabled'],
|
19
|
-
:scope => summary.attributes['scope']
|
20
|
-
}
|
58
|
+
roles << RoleSummary::parse(summary)
|
21
59
|
end
|
22
|
-
res
|
23
60
|
end
|
61
|
+
roles
|
24
62
|
end
|
63
|
+
|
64
|
+
alias_method :roles, :role_listing
|
65
|
+
|
66
|
+
def role_delete(role, scope = Scope::SILO)
|
67
|
+
xml = %Q(<RoleDeleteRequest session-id="#{@session_id}">)
|
68
|
+
xml << %Q(<Role name="#{role}" scope="#{scope}"/>)
|
69
|
+
xml << '</RoleDeleteRequest>'
|
70
|
+
response = execute(xml, '1.2')
|
71
|
+
response.success
|
72
|
+
end
|
73
|
+
|
74
|
+
alias_method :delete_role, :role_delete
|
25
75
|
end
|
26
76
|
|
77
|
+
# Role summary object encapsulating information about a role.
|
78
|
+
#
|
79
|
+
class RoleSummary
|
80
|
+
|
81
|
+
# The short name of the role. Must be unique.
|
82
|
+
attr_accessor :name
|
83
|
+
|
84
|
+
# The full name of the role. Must be unique.
|
85
|
+
attr_accessor :full_name
|
86
|
+
|
87
|
+
# The unique identifier of the role.
|
88
|
+
attr_accessor :id
|
89
|
+
|
90
|
+
# A description of the role.
|
91
|
+
attr_accessor :description
|
92
|
+
|
93
|
+
# Whether or not the role is enabled.
|
94
|
+
attr_accessor :enabled
|
95
|
+
|
96
|
+
# Specifies if the role has global or silo scope.
|
97
|
+
# @see Nexpose::Scope
|
98
|
+
attr_accessor :scope
|
99
|
+
|
100
|
+
def initialize(name, full_name, id, description, enabled = true, scope = Scope::SILO)
|
101
|
+
@name, @full_name, @id, @description, @enabled, @scope = name, full_name, id, description, enabled, scope
|
102
|
+
end
|
103
|
+
|
104
|
+
def self.parse(xml)
|
105
|
+
new(xml.attributes['name'],
|
106
|
+
xml.attributes['full-name'],
|
107
|
+
xml.attributes['id'].to_i,
|
108
|
+
xml.attributes['description'],
|
109
|
+
xml.attributes['enabled'] == 'true',
|
110
|
+
xml.attributes['scope'])
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
class Role < RoleSummary
|
115
|
+
|
116
|
+
# Array of all privileges which are enabled for this role.
|
117
|
+
# Note: Although the underlying XML has different requirements, this only checks for presence.
|
118
|
+
# @see Nexpose::Privilege
|
119
|
+
attr_accessor :privileges
|
120
|
+
|
121
|
+
# Flag to track whether this role exists already on the Nexpose console.
|
122
|
+
# Flag determines behavior of #save method.
|
123
|
+
attr_accessor :existing
|
124
|
+
|
125
|
+
def initialize(name, full_name, id, enabled = true, scope = Scope::SILO)
|
126
|
+
@name, @full_name, @id, @enabled, @scope = name, full_name, id, enabled, scope
|
127
|
+
@privileges = []
|
128
|
+
end
|
129
|
+
|
130
|
+
# Retrieve a detailed description of a single role.
|
131
|
+
#
|
132
|
+
# @param [Connection] nsc Nexpose connection.
|
133
|
+
# @param [String] name The short name of the role.
|
134
|
+
# @param [String] scope Whether the role has global or silo scope. @see Nexpose::Scope
|
135
|
+
# Scope doesn't appear to be required when requesting installed roles.
|
136
|
+
# @return [Role] requested role.
|
137
|
+
#
|
138
|
+
def self.load(nsc, name, scope = Scope::SILO)
|
139
|
+
xml = %Q(<RoleDetailsRequest session-id="#{nsc.session_id}">)
|
140
|
+
xml << %Q(<Role name="#{name}" scope="#{scope}"/>)
|
141
|
+
xml << '</RoleDetailsRequest>'
|
142
|
+
|
143
|
+
response = APIRequest.execute(nsc.url, xml, '1.2')
|
144
|
+
if response.success
|
145
|
+
elem = REXML::XPath.first(response.res, 'RoleDetailsResponse/Role/')
|
146
|
+
parse(elem)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
alias_method :get, :load
|
151
|
+
|
152
|
+
# Create or save a Role to the Nexpose console.
|
153
|
+
#
|
154
|
+
# @param [Connection] nsc Nexpose connection.
|
155
|
+
#
|
156
|
+
def save(nsc)
|
157
|
+
if @existing
|
158
|
+
xml = %Q(<RoleUpdateRequest session-id="#{nsc.session_id}">)
|
159
|
+
xml << to_xml
|
160
|
+
xml << '</RoleUpdateRequest>'
|
161
|
+
else
|
162
|
+
xml = %Q(<RoleCreateRequest session-id="#{nsc.session_id}">)
|
163
|
+
xml << to_xml
|
164
|
+
xml << '</RoleCreateRequest>'
|
165
|
+
end
|
166
|
+
|
167
|
+
response = APIRequest.execute(nsc.url, xml, '1.2')
|
168
|
+
xml = REXML::XPath.first(response.res, 'RoleCreateResponse')
|
169
|
+
@id = xml.attributes['id'].to_i
|
170
|
+
@existing = true
|
171
|
+
response.success
|
172
|
+
end
|
173
|
+
|
174
|
+
# Copy an existing Role to build a new role off of it.
|
175
|
+
# Role will not have a valid name or full_name, so they will need to be provided before saving.
|
176
|
+
#
|
177
|
+
# @param [Connection] nsc Nexpose connection.
|
178
|
+
# @param [String] name The short name of the role which you wish to copy.
|
179
|
+
# @param [String] scope Whether the role has global or silo scope. @see Nexpose::Scope
|
180
|
+
# @return [Role] requested role.
|
181
|
+
#
|
182
|
+
def self.copy(nsc, name, scope = Scope::SILO)
|
183
|
+
role = load(nsc, name, scope)
|
184
|
+
role.name = role.full_name = nil
|
185
|
+
role.id = -1
|
186
|
+
role.existing = false
|
187
|
+
role
|
188
|
+
end
|
189
|
+
|
190
|
+
# Remove this role from the Nexpose console.
|
191
|
+
#
|
192
|
+
# @param [Connection] nsc Nexpose connection.
|
193
|
+
#
|
194
|
+
def delete(nsc)
|
195
|
+
xml = %Q(<RoleDeleteRequest session-id="#{nsc.session_id}">)
|
196
|
+
xml << %Q(<Role name="#{@name}" scope="#{@scope}"/>)
|
197
|
+
xml << '</RoleDeleteRequest>'
|
198
|
+
response = APIRequest.execute(nsc.url, xml, '1.2')
|
199
|
+
response.success
|
200
|
+
end
|
201
|
+
|
202
|
+
def self.parse(xml)
|
203
|
+
role = new(xml.attributes['name'],
|
204
|
+
xml.attributes['full-name'],
|
205
|
+
xml.attributes['id'].to_i,
|
206
|
+
xml.attributes['enabled'] == 'true',
|
207
|
+
xml.attributes['scope'])
|
208
|
+
|
209
|
+
role.description = REXML::XPath.first(xml, 'Description').text
|
210
|
+
role.existing = true
|
211
|
+
|
212
|
+
# Only grab enabled privileges.
|
213
|
+
xml.elements.each("GlobalPrivileges/child::*[@enabled='true']") do |privilege|
|
214
|
+
role.privileges << privilege.name
|
215
|
+
end
|
216
|
+
xml.elements.each("SitePrivileges/child::*[@enabled='true']") do |privilege|
|
217
|
+
role.privileges << privilege.name
|
218
|
+
end
|
219
|
+
xml.elements.each("AssetGroupPrivileges/child::*[@enabled='true']") do |privilege|
|
220
|
+
role.privileges << privilege.name
|
221
|
+
end
|
222
|
+
role
|
223
|
+
end
|
224
|
+
|
225
|
+
def to_xml
|
226
|
+
xml = %Q(<Role name="#{@name}" full-name="#{@full_name}")
|
227
|
+
xml << %Q( enabled="#{(enabled ? 'true' : 'false')}")
|
228
|
+
xml << %Q( scope="#{@scope}">)
|
229
|
+
xml << %Q(<Description>#{@description}</Description>)
|
230
|
+
|
231
|
+
xml << '<SitePrivileges>'
|
232
|
+
Privilege::Site::constants.each do |field|
|
233
|
+
as_s = Privilege::Site.const_get(field)
|
234
|
+
enabled = (privileges.member? as_s) ? 'true' : 'false'
|
235
|
+
xml << %Q(<#{as_s} enabled="#{enabled}"/>)
|
236
|
+
end
|
237
|
+
xml << '</SitePrivileges>'
|
238
|
+
|
239
|
+
xml << '<AssetGroupPrivileges>'
|
240
|
+
Privilege::AssetGroup::constants.each do |field|
|
241
|
+
as_s = Privilege::AssetGroup.const_get(field)
|
242
|
+
enabled = (privileges.member? as_s) ? 'true' : 'false'
|
243
|
+
xml << %Q(<#{as_s} enabled="#{enabled}"/>)
|
244
|
+
end
|
245
|
+
xml << '</AssetGroupPrivileges>'
|
246
|
+
|
247
|
+
xml << '<GlobalPrivileges>'
|
248
|
+
Privilege::Global::constants.each do |field|
|
249
|
+
as_s = Privilege::Global.const_get(field)
|
250
|
+
enabled = (privileges.member? as_s) ? 'true' : 'false'
|
251
|
+
xml << %Q(<#{as_s} enabled="#{enabled}"/>)
|
252
|
+
end
|
253
|
+
xml << '</GlobalPrivileges>'
|
254
|
+
|
255
|
+
xml << '</Role>'
|
256
|
+
xml
|
257
|
+
end
|
258
|
+
end
|
27
259
|
end
|
data/lib/nexpose/scan.rb
CHANGED
@@ -119,27 +119,27 @@ module Nexpose
|
|
119
119
|
# @param [REXML::Document] rexml XML document to parse.
|
120
120
|
# @return [ScanSummary] Scan summary represented by the XML.
|
121
121
|
#
|
122
|
-
def self.parse(
|
123
|
-
tasks = Tasks.parse(
|
124
|
-
nodes = Nodes.parse(
|
125
|
-
vulns = Vulnerabilities.parse(
|
126
|
-
msg =
|
122
|
+
def self.parse(xml)
|
123
|
+
tasks = Tasks.parse(xml.elements['tasks'])
|
124
|
+
nodes = Nodes.parse(xml.elements['nodes'])
|
125
|
+
vulns = Vulnerabilities.parse(xml.attributes['scan-id'], xml)
|
126
|
+
msg = xml.elements['message'] ? xml.elements['message'].text : nil
|
127
127
|
|
128
128
|
# Start time can be empty in some error conditions.
|
129
129
|
start_time = nil
|
130
|
-
unless
|
131
|
-
start_time = DateTime.parse(
|
130
|
+
unless xml.attributes['startTime'] == ''
|
131
|
+
start_time = DateTime.parse(xml.attributes['startTime'].to_s).to_time
|
132
132
|
end
|
133
133
|
|
134
134
|
# End time is often not present, since reporting on running scans.
|
135
135
|
end_time = nil
|
136
|
-
if
|
137
|
-
end_time = DateTime.parse(
|
136
|
+
if xml.attributes['endTime']
|
137
|
+
end_time = DateTime.parse(xml.attributes['endTime'].to_s).to_time
|
138
138
|
end
|
139
|
-
return ScanSummary.new(
|
140
|
-
|
141
|
-
|
142
|
-
|
139
|
+
return ScanSummary.new(xml.attributes['scan-id'].to_i,
|
140
|
+
xml.attributes['site-id'].to_i,
|
141
|
+
xml.attributes['engine-id'].to_i,
|
142
|
+
xml.attributes['status'],
|
143
143
|
start_time,
|
144
144
|
end_time,
|
145
145
|
msg,
|
data/lib/nexpose/site.rb
CHANGED
@@ -14,12 +14,12 @@ module Nexpose
|
|
14
14
|
def site_device_listing(site_id = nil)
|
15
15
|
r = execute(make_xml('SiteDeviceListingRequest', {'site-id' => site_id}))
|
16
16
|
|
17
|
-
|
17
|
+
devices = []
|
18
18
|
if r.success
|
19
|
-
r.res.elements.each('
|
19
|
+
r.res.elements.each('SiteDeviceListingResponse/SiteDevices') do |site|
|
20
20
|
site_id = site.attributes['site-id'].to_i
|
21
|
-
site.elements.each(
|
22
|
-
|
21
|
+
site.elements.each('device') do |device|
|
22
|
+
devices << Device.new(device.attributes['id'].to_i,
|
23
23
|
device.attributes['address'],
|
24
24
|
site_id,
|
25
25
|
device.attributes['riskfactor'].to_f,
|
@@ -27,7 +27,7 @@ module Nexpose
|
|
27
27
|
end
|
28
28
|
end
|
29
29
|
end
|
30
|
-
|
30
|
+
devices
|
31
31
|
end
|
32
32
|
|
33
33
|
alias_method :assets, :site_device_listing
|
@@ -51,7 +51,7 @@ module Nexpose
|
|
51
51
|
r = execute(make_xml('SiteListingRequest'))
|
52
52
|
arr = []
|
53
53
|
if (r.success)
|
54
|
-
r.res.elements.each("
|
54
|
+
r.res.elements.each("SiteListingResponse/SiteSummary") do |site|
|
55
55
|
arr << SiteSummary.new(site.attributes['id'].to_i,
|
56
56
|
site.attributes['name'],
|
57
57
|
site.attributes['description'],
|
@@ -73,13 +73,13 @@ module Nexpose
|
|
73
73
|
#
|
74
74
|
def site_scan_history(site_id)
|
75
75
|
r = execute(make_xml('SiteScanHistoryRequest', {'site-id' => site_id}))
|
76
|
-
|
76
|
+
scans = []
|
77
77
|
if r.success
|
78
|
-
r.res.elements.each(
|
79
|
-
|
78
|
+
r.res.elements.each('SiteScanHistoryResponse/ScanSummary') do |scan_event|
|
79
|
+
scans << ScanSummary.parse(scan_event)
|
80
80
|
end
|
81
81
|
end
|
82
|
-
|
82
|
+
scans
|
83
83
|
end
|
84
84
|
|
85
85
|
# Retrieve the scan summary statistics for the latest completed scan
|
data/lib/nexpose/ticket.rb
CHANGED
@@ -1,7 +1,72 @@
|
|
1
1
|
module Nexpose
|
2
|
+
|
3
|
+
module Ticket
|
4
|
+
|
5
|
+
module State
|
6
|
+
OPEN = 'O'
|
7
|
+
ASSIGNED = 'A'
|
8
|
+
MODIFIED = 'M'
|
9
|
+
FIXED = 'X'
|
10
|
+
PARTIAL = 'P'
|
11
|
+
REJECTED_FIX = 'R'
|
12
|
+
PRIORITIZED = 'Z'
|
13
|
+
NOT_REPRODUCIBLE = 'F'
|
14
|
+
NOT_ISSUE = 'I'
|
15
|
+
CLOSED = 'C'
|
16
|
+
UNKNOWN = 'U'
|
17
|
+
end
|
18
|
+
|
19
|
+
module Priority
|
20
|
+
LOW = 'low'
|
21
|
+
MODERATE = 'moderate'
|
22
|
+
NORMAL = 'normal'
|
23
|
+
HIGH = 'high'
|
24
|
+
CRITICAL = 'critical'
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
2
28
|
module NexposeAPI
|
3
29
|
include XMLUtils
|
4
30
|
|
31
|
+
def ticket_listing
|
32
|
+
xml = make_xml('TicketListingRequest')
|
33
|
+
r = execute(xml, '1.2')
|
34
|
+
tickets = []
|
35
|
+
if r.success
|
36
|
+
r.res.elements.each('TicketListingResponse/TicketSummary') do |summary|
|
37
|
+
tickets << TicketSummary::parse(summary)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
tickets
|
41
|
+
end
|
42
|
+
|
43
|
+
alias_method :tickets, :ticket_listing
|
44
|
+
|
45
|
+
# Deletes a Nexpose ticket.
|
46
|
+
#
|
47
|
+
# @param [Fixnum] ticket Unique ID of the ticket to delete.
|
48
|
+
# @return [Boolean] Whether or not the ticket deletion succeeded.
|
49
|
+
#
|
50
|
+
def delete_ticket(ticket)
|
51
|
+
delete_tickets([ticket])
|
52
|
+
end
|
53
|
+
|
54
|
+
# Deletes a Nexpose ticket.
|
55
|
+
#
|
56
|
+
# @param [Array[Fixnum]] tickets Array of unique IDs of tickets to delete.
|
57
|
+
# @return [Boolean] Whether or not the ticket deletions succeeded.
|
58
|
+
#
|
59
|
+
def delete_tickets(tickets)
|
60
|
+
xml = make_xml('TicketDeleteRequest')
|
61
|
+
tickets.each do |id|
|
62
|
+
xml.add_element('Ticket', {'id' => id})
|
63
|
+
end
|
64
|
+
|
65
|
+
(execute xml, '1.2').success
|
66
|
+
end
|
67
|
+
|
68
|
+
alias_method :ticket_delete, :delete_tickets
|
69
|
+
|
5
70
|
#
|
6
71
|
# Create a Nexpose ticket
|
7
72
|
#
|
@@ -84,25 +149,54 @@ module Nexpose
|
|
84
149
|
false
|
85
150
|
end
|
86
151
|
end
|
152
|
+
end
|
87
153
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
#
|
93
|
-
# @returns {@code true} iff the call was successfull. {@code false} otherwise.
|
94
|
-
#
|
95
|
-
def delete_ticket ticket_ids
|
96
|
-
if not ticket_ids or ticket_ids.count < 1
|
97
|
-
raise ArgumentError.new 'The tickets to delete should not be null or empty'
|
98
|
-
end
|
154
|
+
# Summary of ticket information returned from a ticket listing request.
|
155
|
+
# For more details, issue a ticket detail request.
|
156
|
+
#
|
157
|
+
class TicketSummary
|
99
158
|
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
159
|
+
# The ID number of the ticket.
|
160
|
+
attr_accessor :id
|
161
|
+
|
162
|
+
# Ticket name.
|
163
|
+
attr_accessor :name
|
164
|
+
|
165
|
+
# The asset the ticket is created for.
|
166
|
+
attr_accessor :device_id
|
167
|
+
|
168
|
+
# The login name of person to whom the ticket is assigned.
|
169
|
+
# The user must have view asset privilege on the asset specified in the device-id attribute.
|
170
|
+
attr_accessor :assigned_to
|
171
|
+
|
172
|
+
# The relative priority of the ticket, assigned by the creator of the ticket.
|
173
|
+
# @see Nexpose::Ticket::Priority
|
174
|
+
attr_accessor :priority
|
175
|
+
|
176
|
+
# The login name of the person who created the ticket.
|
177
|
+
attr_accessor :author
|
178
|
+
|
179
|
+
# Date and time of ticket creation.
|
180
|
+
attr_accessor :created_on
|
181
|
+
|
182
|
+
# The current status of the ticket.
|
183
|
+
attr_accessor :state
|
184
|
+
|
185
|
+
def initialize(id, name)
|
186
|
+
@id, @name = id, name
|
187
|
+
end
|
104
188
|
|
105
|
-
|
189
|
+
def self.parse(xml)
|
190
|
+
ticket = new(xml.attributes['id'].to_i,
|
191
|
+
xml.attributes['name'])
|
192
|
+
ticket.device_id = xml.attributes['device-id'].to_i
|
193
|
+
ticket.assigned_to = xml.attributes['assigned-to']
|
194
|
+
ticket.priority = xml.attributes['priority']
|
195
|
+
ticket.author = xml.attributes['author']
|
196
|
+
ticket.created_on = DateTime::parse(xml.attributes['created-on'])
|
197
|
+
lookup = Ticket::State.constants.reduce({}) { |a, e| a[Ticket::State.const_get(e)] = e; a }
|
198
|
+
ticket.state = lookup[xml.attributes['state']]
|
199
|
+
ticket
|
106
200
|
end
|
107
201
|
end
|
108
202
|
end
|
data/lib/nexpose/vuln.rb
CHANGED
@@ -1,4 +1,169 @@
|
|
1
1
|
module Nexpose
|
2
|
+
module NexposeAPI
|
3
|
+
include XMLUtils
|
4
|
+
|
5
|
+
# Retrieve summary details of all vulnerabilities.
|
6
|
+
#
|
7
|
+
# @param [Boolean] full Whether or not to gather the full summary.
|
8
|
+
# Without the flag, only id, title, and severity are returned.
|
9
|
+
# It can take twice a long to retrieve full summary information.
|
10
|
+
# @return [Array[Vulnerability|VulnerabilitySummary]] Collection of all known vulnerabilities.
|
11
|
+
#
|
12
|
+
def vuln_listing(full = false)
|
13
|
+
xml = make_xml('VulnerabilityListingRequest')
|
14
|
+
# TODO Add a flag to do stream parsing of the XML to improve performance.
|
15
|
+
response = execute(xml, '1.2')
|
16
|
+
vulns = []
|
17
|
+
if response.success
|
18
|
+
response.res.elements.each('VulnerabilityListingResponse/VulnerabilitySummary') do |vuln|
|
19
|
+
if full
|
20
|
+
vulns << VulnerabilitySummary::parse(vuln)
|
21
|
+
else
|
22
|
+
vulns << Vulnerability.new(vuln.attributes['id'],
|
23
|
+
vuln.attributes['title'],
|
24
|
+
vuln.attributes['severity'].to_i)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
vulns
|
29
|
+
end
|
30
|
+
|
31
|
+
alias_method :vulns, :vuln_listing
|
32
|
+
|
33
|
+
# Retrieve details for a vulnerability.
|
34
|
+
#
|
35
|
+
# @param [String] vuln_id Nexpose vulnerability ID, such as 'windows-duqu-cve-2011-3402'.
|
36
|
+
# @return [VulnerabilityDetail] Details of the requested vulnerability.
|
37
|
+
#
|
38
|
+
def vuln_details(vuln_id)
|
39
|
+
xml = make_xml('VulnerabilityDetailsRequest', {'vuln-id' => vuln_id})
|
40
|
+
response = execute(xml, '1.2')
|
41
|
+
if response.success
|
42
|
+
response.res.elements.each('VulnerabilityDetailsResponse/Vulnerability') do |vuln|
|
43
|
+
return VulnerabilityDetail::parse(vuln)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Basic vulnerability information. Only includes id, title, and severity.
|
50
|
+
#
|
51
|
+
class Vulnerability
|
52
|
+
|
53
|
+
# The unique ID string for this vulnerability
|
54
|
+
attr_reader :id
|
55
|
+
|
56
|
+
# The title of this vulnerability
|
57
|
+
attr_reader :title
|
58
|
+
|
59
|
+
# How critical the vulnerability is on a scale of 1 to 10.
|
60
|
+
attr_reader :severity
|
61
|
+
|
62
|
+
def initialize(id, title, severity)
|
63
|
+
@id, @title, @severity = id, title, severity
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Summary of a vulnerability.
|
68
|
+
#
|
69
|
+
class VulnerabilitySummary < Vulnerability
|
70
|
+
|
71
|
+
# PCI severity value for the vulnerability on a scale of 1 to 5.
|
72
|
+
attr_accessor :pci_severity
|
73
|
+
|
74
|
+
# Whether all checks for the vulnerability are safe.
|
75
|
+
# Unsafe checks may cause denial of service or otherwise disrupt system performance.
|
76
|
+
attr_accessor :safe
|
77
|
+
|
78
|
+
# A vulnerability is considered “credentialed” when all of its checks
|
79
|
+
# require credentials or if the check depends on previous authentication
|
80
|
+
# during a scan.
|
81
|
+
attr_accessor :credentials
|
82
|
+
|
83
|
+
# When this vulnerability was first included in the application.
|
84
|
+
attr_accessor :added
|
85
|
+
|
86
|
+
# The last date the vulnerability was modified.
|
87
|
+
attr_accessor :modified
|
88
|
+
|
89
|
+
# The date when the information about the vulnerability was first released.
|
90
|
+
attr_accessor :published
|
91
|
+
|
92
|
+
# How the vulnerability is exploited according to PCI standards.
|
93
|
+
attr_accessor :cvss_vector
|
94
|
+
|
95
|
+
# The computation of the Common Vulnerability Scoring System indicating
|
96
|
+
# compliance with PCI standards on a scale from 0 to 10.0.
|
97
|
+
attr_accessor :cvss_score
|
98
|
+
|
99
|
+
def self.parse_attributes(xml)
|
100
|
+
vuln = new(xml.attributes['id'],
|
101
|
+
xml.attributes['title'],
|
102
|
+
xml.attributes['severity'].to_i)
|
103
|
+
|
104
|
+
vuln.pci_severity = xml.attributes['pciSeverity'].to_i
|
105
|
+
vuln.safe = xml.attributes['safe'] == 'true' # or xml.attributes['safe'] == '1'
|
106
|
+
vuln.added = Date::parse(xml.attributes['added'])
|
107
|
+
vuln.modified = Date::parse(xml.attributes['modified'])
|
108
|
+
vuln.credentials = xml.attributes['requiresCredentials'] == 'true'
|
109
|
+
|
110
|
+
# These three fields are optional in the XSD.
|
111
|
+
vuln.published = Date::parse(xml.attributes['published']) if xml.attributes['published']
|
112
|
+
vuln.cvss_vector = xml.attributes['cvssVector'] if xml.attributes['cvssVector']
|
113
|
+
vuln.cvss_score = xml.attributes['cvssScore'].to_f if xml.attributes['cvssScore']
|
114
|
+
vuln
|
115
|
+
end
|
116
|
+
|
117
|
+
def self.parse(xml)
|
118
|
+
parse_attributes(xml)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# Details for a vulnerability.
|
123
|
+
#
|
124
|
+
class VulnerabilityDetail < VulnerabilitySummary
|
125
|
+
|
126
|
+
# The HTML Description of this vulnerability.
|
127
|
+
attr_accessor :description
|
128
|
+
|
129
|
+
# External References for this vulnerability.
|
130
|
+
# Array containing (Reference)
|
131
|
+
attr_accessor :references
|
132
|
+
|
133
|
+
# The HTML Solution for this vulnerability.
|
134
|
+
attr_accessor :solution
|
135
|
+
|
136
|
+
def initialize(id, title, severity)
|
137
|
+
@id, @title, @severity = id, title, severity
|
138
|
+
@references = []
|
139
|
+
end
|
140
|
+
|
141
|
+
def self.parse(xml)
|
142
|
+
vuln = parse_attributes(xml)
|
143
|
+
|
144
|
+
vuln.description = REXML::XPath.first(xml, 'description').text
|
145
|
+
vuln.solution = REXML::XPath.first(xml, 'solution').text
|
146
|
+
|
147
|
+
xml.elements.each('references/reference') do |ref|
|
148
|
+
vuln.references << Reference.new(ref.attributes['source'], ref.text)
|
149
|
+
end
|
150
|
+
vuln
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
# Reference information for a Vulnerability.
|
155
|
+
#
|
156
|
+
class Reference
|
157
|
+
|
158
|
+
attr_reader :source
|
159
|
+
attr_reader :reference
|
160
|
+
|
161
|
+
def initialize(source, reference)
|
162
|
+
@source = source
|
163
|
+
@reference = reference
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
2
167
|
module NexposeAPI
|
3
168
|
include XMLUtils
|
4
169
|
|
@@ -13,7 +178,7 @@ module Nexpose
|
|
13
178
|
# @param status - (optional) The status of the vulnerability exception:
|
14
179
|
# "Under Review", "Approved", "Rejected"
|
15
180
|
#-----------------------------------------------------------------------
|
16
|
-
def
|
181
|
+
def vuln_exception_listing(status = nil)
|
17
182
|
option = {}
|
18
183
|
|
19
184
|
if status && !status.empty?
|
@@ -328,182 +493,4 @@ module Nexpose
|
|
328
493
|
r.success
|
329
494
|
end
|
330
495
|
end
|
331
|
-
|
332
|
-
# === Description
|
333
|
-
# Object that represents a listing of all of the vulnerabilities in the vulnerability database
|
334
|
-
#
|
335
|
-
class VulnerabilityListing
|
336
|
-
# true if an error condition exists; false otherwise
|
337
|
-
attr_reader :error
|
338
|
-
# Error message string
|
339
|
-
attr_reader :error_msg
|
340
|
-
# The NSC Connection associated with this object
|
341
|
-
attr_reader :connection
|
342
|
-
# Array containing (VulnerabilitySummary*)
|
343
|
-
attr_reader :vulnerability_summaries
|
344
|
-
# The number of vulnerability definitions
|
345
|
-
attr_reader :vulnerability_count
|
346
|
-
|
347
|
-
# Constructor
|
348
|
-
# VulnerabilityListing(connection)
|
349
|
-
def initialize(connection)
|
350
|
-
@error = false
|
351
|
-
@vulnerability_summaries = []
|
352
|
-
@connection = connection
|
353
|
-
|
354
|
-
r = @connection.execute('<VulnerabilityListingRequest session-id="' + @connection.session_id + '"/>')
|
355
|
-
|
356
|
-
if r.success
|
357
|
-
r.res.elements.each('VulnerabilityListingResponse/VulnerabilitySummary') do |v|
|
358
|
-
@vulnerability_summaries.push(VulnerabilitySummary.new(v.attributes['id'], v.attributes['title'], v.attributes['severity']))
|
359
|
-
end
|
360
|
-
else
|
361
|
-
@error = true
|
362
|
-
@error_msg = 'VulnerabilitySummary Parse Error'
|
363
|
-
end
|
364
|
-
@vulnerability_count = @vulnerability_summaries.length
|
365
|
-
end
|
366
|
-
end
|
367
|
-
|
368
|
-
# === Description
|
369
|
-
# Object that represents the summary of an entry in the vulnerability database
|
370
|
-
#
|
371
|
-
class VulnerabilitySummary
|
372
|
-
|
373
|
-
# The unique ID string for this vulnerability
|
374
|
-
attr_reader :id
|
375
|
-
# The title of this vulnerability
|
376
|
-
attr_reader :title
|
377
|
-
# The severity of this vulnerability (1 – 10)
|
378
|
-
attr_reader :severity
|
379
|
-
|
380
|
-
# Constructor
|
381
|
-
# VulnerabilitySummary(id, title, severity)
|
382
|
-
def initialize(id, title, severity)
|
383
|
-
@id = id
|
384
|
-
@title = title
|
385
|
-
@severity = severity
|
386
|
-
|
387
|
-
end
|
388
|
-
|
389
|
-
end
|
390
|
-
|
391
|
-
# === Description
|
392
|
-
# Object that represents the details for an entry in the vulnerability database
|
393
|
-
#
|
394
|
-
class VulnerabilityDetail
|
395
|
-
# true if an error condition exists; false otherwise
|
396
|
-
attr_reader :error
|
397
|
-
# Error message string
|
398
|
-
attr_reader :error_msg
|
399
|
-
# The NSC Connection associated with this object
|
400
|
-
attr_reader :connection
|
401
|
-
# The unique ID string for this vulnerability
|
402
|
-
attr_reader :id
|
403
|
-
# The title of this vulnerability
|
404
|
-
attr_reader :title
|
405
|
-
# The severity of this vulnerability (1 – 10)
|
406
|
-
attr_reader :severity
|
407
|
-
# The pciSeverity of this vulnerability
|
408
|
-
attr_reader :pciSeverity
|
409
|
-
# The CVSS score of this vulnerability
|
410
|
-
attr_reader :cvssScore
|
411
|
-
# The CVSS vector of this vulnerability
|
412
|
-
attr_reader :cvssVector
|
413
|
-
# The date this vulnerability was published
|
414
|
-
attr_reader :published
|
415
|
-
# The date this vulnerability was added to Nexpose
|
416
|
-
attr_reader :added
|
417
|
-
# The last date this vulnerability was modified
|
418
|
-
attr_reader :modified
|
419
|
-
# The HTML Description of this vulnerability
|
420
|
-
attr_reader :description
|
421
|
-
# External References for this vulnerability
|
422
|
-
# Array containing (Reference)
|
423
|
-
attr_reader :references
|
424
|
-
# The HTML Solution for this vulnerability
|
425
|
-
attr_reader :solution
|
426
|
-
|
427
|
-
# Constructor
|
428
|
-
# VulnerabilityListing(connection,id)
|
429
|
-
def initialize(connection, id)
|
430
|
-
|
431
|
-
@error = false
|
432
|
-
@connection = connection
|
433
|
-
@id = id
|
434
|
-
@references = []
|
435
|
-
|
436
|
-
r = @connection.execute('<VulnerabilityDetailsRequest session-id="' + @connection.session_id + '" vuln-id="' + @id + '"/>')
|
437
|
-
|
438
|
-
if r.success
|
439
|
-
r.res.elements.each('VulnerabilityDetailsResponse/Vulnerability') do |v|
|
440
|
-
@id = v.attributes['id']
|
441
|
-
@title = v.attributes['title']
|
442
|
-
@severity = v.attributes['severity']
|
443
|
-
@pciSeverity = v.attributes['pciSeverity']
|
444
|
-
@cvssScore = v.attributes['cvssScore']
|
445
|
-
@cvssVector = v.attributes['cvssVector']
|
446
|
-
@published = v.attributes['published']
|
447
|
-
@added = v.attributes['added']
|
448
|
-
@modified = v.attributes['modified']
|
449
|
-
|
450
|
-
v.elements.each('description') do |desc|
|
451
|
-
@description = desc.to_s.gsub(/\<\/?description\>/i, '')
|
452
|
-
end
|
453
|
-
|
454
|
-
v.elements.each('solution') do |sol|
|
455
|
-
@solution = sol.to_s.gsub(/\<\/?solution\>/i, '')
|
456
|
-
end
|
457
|
-
|
458
|
-
v.elements.each('references/reference') do |ref|
|
459
|
-
@references.push(Reference.new(ref.attributes['source'], ref.text))
|
460
|
-
end
|
461
|
-
end
|
462
|
-
else
|
463
|
-
@error = true
|
464
|
-
@error_msg = 'VulnerabilitySummary Parse Error'
|
465
|
-
end
|
466
|
-
|
467
|
-
end
|
468
|
-
end
|
469
|
-
|
470
|
-
# === Description
|
471
|
-
#
|
472
|
-
class Reference
|
473
|
-
|
474
|
-
attr_reader :source
|
475
|
-
attr_reader :reference
|
476
|
-
|
477
|
-
def initialize(source, reference)
|
478
|
-
@source = source
|
479
|
-
@reference = reference
|
480
|
-
end
|
481
|
-
end
|
482
|
-
|
483
|
-
# TODO: review
|
484
|
-
# === Description
|
485
|
-
#
|
486
|
-
class VulnFilter
|
487
|
-
|
488
|
-
attr_reader :typeMask
|
489
|
-
attr_reader :maxAlerts
|
490
|
-
attr_reader :severityThreshold
|
491
|
-
|
492
|
-
def initialize(type_mask, severity_threshold, max_alerts = -1)
|
493
|
-
@typeMask = type_mask
|
494
|
-
@maxAlerts = max_alerts
|
495
|
-
@severityThreshold = severity_threshold
|
496
|
-
end
|
497
|
-
|
498
|
-
include Sanitize
|
499
|
-
|
500
|
-
def to_xml
|
501
|
-
xml = '<vuln_filter '
|
502
|
-
xml << %Q{ type_mask="#{replace_entities(typeMask)}"}
|
503
|
-
xml << %Q{ maxAlerts="#{replace_entities(maxAlerts)}"}
|
504
|
-
xml << %Q{ severity_threshold="#{replace_entities(severityThreshold)}"}
|
505
|
-
xml << '/>'
|
506
|
-
xml
|
507
|
-
end
|
508
|
-
end
|
509
496
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: nexpose
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -11,7 +11,7 @@ authors:
|
|
11
11
|
autorequire:
|
12
12
|
bindir: bin
|
13
13
|
cert_chain: []
|
14
|
-
date: 2013-
|
14
|
+
date: 2013-05-06 00:00:00.000000000 Z
|
15
15
|
dependencies:
|
16
16
|
- !ruby/object:Gem::Dependency
|
17
17
|
name: librex
|
@@ -20,7 +20,7 @@ dependencies:
|
|
20
20
|
requirements:
|
21
21
|
- - ! '>='
|
22
22
|
- !ruby/object:Gem::Version
|
23
|
-
version: 0.0.
|
23
|
+
version: 0.0.68
|
24
24
|
type: :runtime
|
25
25
|
prerelease: false
|
26
26
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -28,7 +28,7 @@ dependencies:
|
|
28
28
|
requirements:
|
29
29
|
- - ! '>='
|
30
30
|
- !ruby/object:Gem::Version
|
31
|
-
version: 0.0.
|
31
|
+
version: 0.0.68
|
32
32
|
- !ruby/object:Gem::Dependency
|
33
33
|
name: rex
|
34
34
|
requirement: !ruby/object:Gem::Requirement
|