nexpose 0.6.5 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/COPYING +33 -0
- data/README.markdown +6 -2
- data/lib/nexpose.rb +6 -0
- data/lib/nexpose/ajax.rb +27 -6
- data/lib/nexpose/dag.rb +5 -13
- data/lib/nexpose/data_table.rb +1 -1
- data/lib/nexpose/discovery.rb +191 -0
- data/lib/nexpose/discovery/filter.rb +36 -0
- data/lib/nexpose/filter.rb +17 -6
- data/lib/nexpose/group.rb +21 -8
- data/lib/nexpose/multi_tenant_user.rb +219 -0
- data/lib/nexpose/report_template.rb +1 -1
- data/lib/nexpose/scan.rb +11 -0
- data/lib/nexpose/silo.rb +282 -278
- data/lib/nexpose/silo_profile.rb +239 -0
- data/lib/nexpose/site.rb +112 -5
- data/lib/nexpose/tag.rb +343 -0
- data/lib/nexpose/tag/criteria.rb +45 -0
- metadata +9 -2
@@ -0,0 +1,239 @@
|
|
1
|
+
module Nexpose
|
2
|
+
|
3
|
+
class Connection
|
4
|
+
include XMLUtils
|
5
|
+
|
6
|
+
# Retrieve a list of all silos the user is authorized to view or manage.
|
7
|
+
#
|
8
|
+
# @return [Array[SiloProfileSummary]] Array of SiloSummary objects.
|
9
|
+
#
|
10
|
+
def list_silo_profiles
|
11
|
+
r = execute(make_xml('SiloProfileListingRequest'), '1.2')
|
12
|
+
arr = []
|
13
|
+
if r.success
|
14
|
+
r.res.elements.each('SiloProfileListingResponse/SiloProfileSummaries/SiloProfileSummary') do |profile|
|
15
|
+
arr << SiloProfileSummary.parse(profile)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
arr
|
19
|
+
end
|
20
|
+
|
21
|
+
alias_method :silo_profiles, :list_silo_profiles
|
22
|
+
|
23
|
+
# Delete the specified silo profile
|
24
|
+
#
|
25
|
+
# @return Whether or not the delete request succeeded.
|
26
|
+
#
|
27
|
+
def delete_silo_profile(silo_profile_id)
|
28
|
+
r = execute(make_xml('SiloProfileDeleteRequest', {'silo-profile-id' => silo_profile_id}), '1.2')
|
29
|
+
r.success
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class SiloProfile
|
34
|
+
attr_accessor :id
|
35
|
+
attr_accessor :name
|
36
|
+
attr_accessor :description
|
37
|
+
attr_accessor :all_licensed_modules
|
38
|
+
attr_accessor :all_global_engines
|
39
|
+
attr_accessor :all_global_report_templates
|
40
|
+
attr_accessor :all_global_scan_templates
|
41
|
+
attr_accessor :global_report_templates
|
42
|
+
attr_accessor :global_scan_engines
|
43
|
+
attr_accessor :global_scan_templates
|
44
|
+
attr_accessor :licensed_modules
|
45
|
+
attr_accessor :restricted_report_formats
|
46
|
+
attr_accessor :restricted_report_sections
|
47
|
+
|
48
|
+
def initialize(&block)
|
49
|
+
instance_eval &block if block_given?
|
50
|
+
@global_report_templates = Array(@global_report_templates)
|
51
|
+
@global_scan_engines = Array(@global_scan_engines)
|
52
|
+
@global_scan_templates = Array(@global_scan_templates)
|
53
|
+
@licensed_modules = Array(@licensed_modules)
|
54
|
+
@restricted_report_formats = Array(@restricted_report_formats)
|
55
|
+
@restricted_report_sections = Array(@restricted_report_sections)
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.copy(connection, id)
|
59
|
+
profile = load(connection, id)
|
60
|
+
profile.id = nil
|
61
|
+
profile.name = nil
|
62
|
+
profile
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.load(connection, id)
|
66
|
+
r = connection.execute(connection.make_xml('SiloProfileConfigRequest', {'silo-profile-id' => id}), '1.2')
|
67
|
+
|
68
|
+
if r.success
|
69
|
+
r.res.elements.each('SiloProfileConfigResponse/SiloProfileConfig') do |config|
|
70
|
+
return SiloProfile.parse(config)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
nil
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.parse(xml)
|
77
|
+
new do |profile|
|
78
|
+
profile.id = xml.attributes['id']
|
79
|
+
profile.name = xml.attributes['name']
|
80
|
+
profile.description = xml.attributes['description']
|
81
|
+
profile.all_licensed_modules = xml.attributes['all-licensed-modules'].to_s.chomp.eql?('true')
|
82
|
+
profile.all_global_engines = xml.attributes['all-global-engines'].to_s.chomp.eql?('true')
|
83
|
+
profile.all_global_report_templates = xml.attributes['all-global-report-templates'].to_s.chomp.eql?('true')
|
84
|
+
profile.all_global_scan_templates = xml.attributes['all-global-scan-templates'].to_s.chomp.eql?('true')
|
85
|
+
|
86
|
+
profile.global_report_templates = []
|
87
|
+
xml.elements.each('GlobalReportTemplates/GlobalReportTemplate') { |template| profile.global_report_templates << template.attributes['name'] }
|
88
|
+
|
89
|
+
profile.global_scan_engines = []
|
90
|
+
xml.elements.each('GlobalScanEngines/GlobalScanEngine') { |engine| profile.global_scan_engines << engine.attributes['name'] }
|
91
|
+
|
92
|
+
profile.global_scan_templates = []
|
93
|
+
xml.elements.each('GlobalScanTemplates/GlobalScanTemplate') { |template| profile.global_scan_templates << template.attributes['name'] }
|
94
|
+
|
95
|
+
profile.licensed_modules = []
|
96
|
+
xml.elements.each('LicensedModules/LicensedModule') { |license_module| profile.licensed_modules << license_module.attributes['name'] }
|
97
|
+
|
98
|
+
profile.restricted_report_formats = []
|
99
|
+
xml.elements.each('RestrictedReportFormats/RestrictedReportFormat') { |format| profile.restricted_report_formats << format.attributes['name'] }
|
100
|
+
|
101
|
+
profile.restricted_report_sections = []
|
102
|
+
xml.elements.each('RestrictedReportSections/RestrictedReportSection') { |section| profile.restricted_report_sections << section.attributes['name'] }
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def save(connection)
|
107
|
+
begin
|
108
|
+
update(connection)
|
109
|
+
rescue APIError => error
|
110
|
+
raise error unless (error.message =~ /A silo profile .* does not exist./)
|
111
|
+
create(connection)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
# Updates an existing silo profile on a Nexpose console.
|
116
|
+
#
|
117
|
+
# @param [Connection] connection Connection to console where this silo profile will be saved.
|
118
|
+
# @return [String] Silo Profile ID assigned to this configuration, if successful.
|
119
|
+
#
|
120
|
+
def update(connection)
|
121
|
+
xml = connection.make_xml('SiloProfileUpdateRequest')
|
122
|
+
xml.add_element(as_xml)
|
123
|
+
r = connection.execute(xml, '1.2')
|
124
|
+
@id = r.attributes['silo-profile-id'] if r.success
|
125
|
+
end
|
126
|
+
|
127
|
+
# Saves a new silo profile to a Nexpose console.
|
128
|
+
#
|
129
|
+
# @param [Connection] connection Connection to console where this silo profile will be saved.
|
130
|
+
# @return [String] Silo Profile ID assigned to this configuration, if successful.
|
131
|
+
#
|
132
|
+
def create(connection)
|
133
|
+
xml = connection.make_xml('SiloProfileCreateRequest')
|
134
|
+
xml.add_element(as_xml)
|
135
|
+
r = connection.execute(xml, '1.2')
|
136
|
+
@id = r.attributes['silo-profile-id'] if r.success
|
137
|
+
end
|
138
|
+
|
139
|
+
def delete(connection)
|
140
|
+
connection.delete_silo_profile(@id)
|
141
|
+
end
|
142
|
+
|
143
|
+
def as_xml
|
144
|
+
xml = REXML::Element.new('SiloProfileConfig')
|
145
|
+
xml.add_attributes({'id' => @id,
|
146
|
+
'name' => @name,
|
147
|
+
'description' => @description,
|
148
|
+
'all-licensed-modules' => @all_licensed_modules,
|
149
|
+
'all-global-engines' => @all_global_engines,
|
150
|
+
'all-global-report-templates' => @all_global_report_templates,
|
151
|
+
'all-global-scan-templates' => @all_global_scan_templates})
|
152
|
+
|
153
|
+
unless @global_report_templates.empty?
|
154
|
+
templates = xml.add_element('GlobalReportTemplates')
|
155
|
+
@global_report_templates.each do |template|
|
156
|
+
templates.add_element('GlobalReportTemplate', {'name' => template})
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
unless @global_scan_engines.empty?
|
161
|
+
engines = xml.add_element('GlobalScanEngines')
|
162
|
+
@global_report_templates.each do |engine|
|
163
|
+
engines.add_element('GlobalScanEngine', {'name' => engine})
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
unless @global_scan_templates.empty?
|
168
|
+
templates = xml.add_element('GlobalScanTemplates')
|
169
|
+
@global_scan_templates.each do |template|
|
170
|
+
templates.add_element('GlobalScanTemplate', {'name' => template})
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
unless @licensed_modules.empty?
|
175
|
+
licensed_modules = xml.add_element('LicensedModules')
|
176
|
+
@licensed_modules.each do |licensed_module|
|
177
|
+
licensed_modules.add_element('LicensedModule', {'name' => licensed_module})
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
unless @restricted_report_formats.empty?
|
182
|
+
formats = xml.add_element('RestrictedReportFormats')
|
183
|
+
@restricted_report_formats.each do |format|
|
184
|
+
formats.add_element('RestrictedReportFormat', {'name' => format})
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
unless @restricted_report_sections.empty?
|
189
|
+
sections = xml.add_element('RestrictedReportSections')
|
190
|
+
@restricted_report_sections.each do |section|
|
191
|
+
sections.add_element('RestrictedReportSection', {'name' => section})
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
xml
|
196
|
+
end
|
197
|
+
|
198
|
+
def to_xml
|
199
|
+
as_xml.to_s
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
class SiloProfileSummary
|
204
|
+
attr_reader :id
|
205
|
+
attr_reader :name
|
206
|
+
attr_reader :description
|
207
|
+
attr_reader :global_report_template_count
|
208
|
+
attr_reader :global_scan_engine_count
|
209
|
+
attr_reader :global_scan_template_count
|
210
|
+
attr_reader :licensed_module_count
|
211
|
+
attr_reader :restricted_report_section_count
|
212
|
+
attr_reader :all_licensed_modules
|
213
|
+
attr_reader :all_global_engines
|
214
|
+
attr_reader :all_global_report_templates
|
215
|
+
attr_reader :all_global_scan_templates
|
216
|
+
|
217
|
+
|
218
|
+
def initialize(&block)
|
219
|
+
instance_eval &block if block_given?
|
220
|
+
end
|
221
|
+
|
222
|
+
def self.parse(xml)
|
223
|
+
new do
|
224
|
+
@id = xml.attributes['id']
|
225
|
+
@name = xml.attributes['name']
|
226
|
+
@description = xml.attributes['description']
|
227
|
+
@global_report_template_count = xml.attributes['global-report-template-count']
|
228
|
+
@global_scan_engine_count = xml.attributes['global-scan-engine-count']
|
229
|
+
@global_scan_template_count = xml.attributes['global-scan-template-count']
|
230
|
+
@licensed_module_count = xml.attributes['licensed-module-count']
|
231
|
+
@restricted_report_section_count = xml.attributes['restricted-report-section-count']
|
232
|
+
@all_licensed_modules = xml.attributes['all-licensed-modules']
|
233
|
+
@all_global_engines = xml.attributes['all-global-engines']
|
234
|
+
@all_global_report_templates = xml.attributes['all-global-report-templates']
|
235
|
+
@all_global_scan_templates = xml.attributes['all-global-scan-templates']
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
data/lib/nexpose/site.rb
CHANGED
@@ -125,14 +125,22 @@ module Nexpose
|
|
125
125
|
|
126
126
|
# Whether or not this site is dynamic.
|
127
127
|
# Dynamic sites are created through Asset Discovery Connections.
|
128
|
-
# Modifying their behavior through the API is not recommended.
|
129
128
|
attr_accessor :is_dynamic
|
130
129
|
|
130
|
+
# Asset filter criteria if this site is dynamic.
|
131
|
+
attr_accessor :criteria
|
132
|
+
|
133
|
+
# ID of the discovery connection associated with this site if it is dynamic.
|
134
|
+
attr_accessor :discovery_connection_id
|
135
|
+
|
136
|
+
# [Array[TagSummary]] Collection of TagSummary
|
137
|
+
attr_accessor :tags
|
138
|
+
|
131
139
|
# Site constructor. Both arguments are optional.
|
132
140
|
#
|
133
141
|
# @param [String] name Unique name of the site.
|
134
142
|
# @param [String] scan_template ID of the scan template to use.
|
135
|
-
def initialize(name = nil, scan_template = 'full-audit')
|
143
|
+
def initialize(name = nil, scan_template = 'full-audit-without-web-spider')
|
136
144
|
@name = name
|
137
145
|
@scan_template = scan_template
|
138
146
|
|
@@ -146,6 +154,7 @@ module Nexpose
|
|
146
154
|
@alerts = []
|
147
155
|
@exclude = []
|
148
156
|
@users = []
|
157
|
+
@tags = []
|
149
158
|
end
|
150
159
|
|
151
160
|
# Returns true when the site is dynamic.
|
@@ -153,6 +162,11 @@ module Nexpose
|
|
153
162
|
is_dynamic
|
154
163
|
end
|
155
164
|
|
165
|
+
def discovery_connection_id=(value)
|
166
|
+
@is_dynamic = true
|
167
|
+
@discovery_connection_id = value.to_i
|
168
|
+
end
|
169
|
+
|
156
170
|
# Adds an asset to this site by host name.
|
157
171
|
#
|
158
172
|
# @param [String] hostname FQDN or DNS-resolvable host name of an asset.
|
@@ -203,7 +217,9 @@ module Nexpose
|
|
203
217
|
def self.load(connection, id)
|
204
218
|
r = APIRequest.execute(connection.url,
|
205
219
|
%(<SiteConfigRequest session-id="#{connection.session_id}" site-id="#{id}"/>))
|
206
|
-
parse(r.res)
|
220
|
+
site = parse(r.res)
|
221
|
+
site.load_dynamic_attributes(connection) if site.dynamic?
|
222
|
+
site
|
207
223
|
end
|
208
224
|
|
209
225
|
# Copy an existing configuration from a Nexpose instance.
|
@@ -222,13 +238,31 @@ module Nexpose
|
|
222
238
|
end
|
223
239
|
|
224
240
|
# Saves this site to a Nexpose console.
|
241
|
+
# If the site is dynamic, connection and asset filter changes must be
|
242
|
+
# saved through the DiscoveryConnection#update_site call.
|
225
243
|
#
|
226
244
|
# @param [Connection] connection Connection to console where this site will be saved.
|
227
245
|
# @return [Fixnum] Site ID assigned to this configuration, if successful.
|
228
246
|
#
|
229
247
|
def save(connection)
|
230
|
-
|
231
|
-
|
248
|
+
if dynamic?
|
249
|
+
raise APIError.new(nil, 'Cannot save a dynamic site without a discovery connection configured.') unless @discovery_connection_id
|
250
|
+
|
251
|
+
new_site = @id == -1
|
252
|
+
save_dynamic_criteria(connection) if new_site
|
253
|
+
|
254
|
+
# Have to retrieve and attach shared creds, or saving will fail.
|
255
|
+
xml = _append_shared_creds_to_xml(connection, as_xml)
|
256
|
+
response = AJAX.post(connection, '/ajax/save_site_config.txml', xml)
|
257
|
+
saved = REXML::XPath.first(REXML::Document.new(response), 'SaveConfig')
|
258
|
+
raise APIError.new(response, 'Failed to save dynamic site.') if saved.nil? || saved.attributes['success'].to_i != 1
|
259
|
+
|
260
|
+
save_dynamic_criteria(connection) unless new_site
|
261
|
+
else
|
262
|
+
r = connection.execute('<SiteSaveRequest session-id="' + connection.session_id + '">' + to_xml + ' </SiteSaveRequest>')
|
263
|
+
@id = r.attributes['site-id'].to_i if r.success
|
264
|
+
end
|
265
|
+
@id
|
232
266
|
end
|
233
267
|
|
234
268
|
# Delete this site from a Nexpose console.
|
@@ -257,6 +291,38 @@ module Nexpose
|
|
257
291
|
Scan.parse(response.res) if response.success
|
258
292
|
end
|
259
293
|
|
294
|
+
# Save only the criteria of a dynamic site.
|
295
|
+
#
|
296
|
+
# @param [Connection] nsc Connection to a console.
|
297
|
+
# @return [Fixnum] Site ID.
|
298
|
+
#
|
299
|
+
def save_dynamic_criteria(nsc)
|
300
|
+
params = to_dynamic_map
|
301
|
+
response = AJAX.form_post(nsc, '/data/site/saveSite', params)
|
302
|
+
json = JSON.parse(response)
|
303
|
+
if json['response'] =~ /success/
|
304
|
+
if @id < 1
|
305
|
+
@id = json['entityID'].to_i
|
306
|
+
end
|
307
|
+
else
|
308
|
+
raise APIError.new(response, json['message'])
|
309
|
+
end
|
310
|
+
@id
|
311
|
+
end
|
312
|
+
|
313
|
+
# Retrieve the currrent filter criteria used by a dynamic site.
|
314
|
+
#
|
315
|
+
# @param [Connection] nsc Connection to a console.
|
316
|
+
# @param [Fixnum] site_id ID of an existing site.
|
317
|
+
# @return [Criteria] Current criteria for the site.
|
318
|
+
#
|
319
|
+
def load_dynamic_attributes(nsc)
|
320
|
+
response = AJAX.get(nsc, "/data/site/loadDynamicSite?entityid=#{@id}")
|
321
|
+
json = JSON.parse(response)
|
322
|
+
@discovery_connection_id = json['discoveryConfigs']['id']
|
323
|
+
@criteria = Criteria.parse(json['searchCriteria'])
|
324
|
+
end
|
325
|
+
|
260
326
|
include Sanitize
|
261
327
|
|
262
328
|
# Generate an XML representation of this site configuration
|
@@ -269,6 +335,7 @@ module Nexpose
|
|
269
335
|
xml.attributes['name'] = @name
|
270
336
|
xml.attributes['description'] = @description
|
271
337
|
xml.attributes['riskfactor'] = @risk_factor
|
338
|
+
xml.attributes['isDynamic'] == '1' if dynamic?
|
272
339
|
|
273
340
|
unless @users.empty?
|
274
341
|
elem = REXML::Element.new('Users')
|
@@ -309,6 +376,11 @@ module Nexpose
|
|
309
376
|
elem.add_element(sched)
|
310
377
|
xml.add_element(elem)
|
311
378
|
|
379
|
+
unless tags.empty?
|
380
|
+
tag_xml = xml.add_element(REXML::Element.new('Tags'))
|
381
|
+
@tags.each { |tag| tag_xml.add_element(tag.as_xml) }
|
382
|
+
end
|
383
|
+
|
312
384
|
xml
|
313
385
|
end
|
314
386
|
|
@@ -316,6 +388,19 @@ module Nexpose
|
|
316
388
|
as_xml.to_s
|
317
389
|
end
|
318
390
|
|
391
|
+
def to_dynamic_map
|
392
|
+
details = { 'dynamic' => true,
|
393
|
+
'name' => @name,
|
394
|
+
'tag' => @description.nil? ? '' : @description,
|
395
|
+
'riskFactor' => @risk_factor,
|
396
|
+
# 'vCenter' => @discovery_connection_id,
|
397
|
+
'searchCriteria' => @criteria.nil? ? { 'operator' => 'AND' } : @criteria.to_map }
|
398
|
+
params = { 'configID' => @discovery_connection_id,
|
399
|
+
'entityid' => @id > 0 ? @id : false,
|
400
|
+
'mode' => @id > 0 ? 'edit' : false,
|
401
|
+
'entityDetails' => details }
|
402
|
+
end
|
403
|
+
|
319
404
|
# Parse a response from a Nexpose console into a valid Site object.
|
320
405
|
#
|
321
406
|
# @param [REXML::Document] rexml XML document to parse.
|
@@ -370,10 +455,32 @@ module Nexpose
|
|
370
455
|
site.alerts << Alert.parse(alert)
|
371
456
|
end
|
372
457
|
|
458
|
+
s.elements.each('Tags/Tag') do |tag|
|
459
|
+
site.tags << TagSummary.parse_xml(tag)
|
460
|
+
end
|
461
|
+
|
373
462
|
return site
|
374
463
|
end
|
375
464
|
nil
|
376
465
|
end
|
466
|
+
|
467
|
+
def _append_shared_creds_to_xml(connection, xml)
|
468
|
+
xml_w_creds = AJAX.get(connection, "/ajax/site_config.txml?siteid=#{@id}")
|
469
|
+
cred_xml = REXML::XPath.first(REXML::Document.new(xml_w_creds), 'Site/Credentials')
|
470
|
+
unless cred_xml.nil?
|
471
|
+
creds = REXML::XPath.first(xml, 'Credentials')
|
472
|
+
if creds.nil?
|
473
|
+
xml.add_element(cred_xml)
|
474
|
+
else
|
475
|
+
cred_xml.elements.each do |cred|
|
476
|
+
if cred.attributes['shared'].to_i == 1
|
477
|
+
creds.add_element(cred)
|
478
|
+
end
|
479
|
+
end
|
480
|
+
end
|
481
|
+
end
|
482
|
+
xml
|
483
|
+
end
|
377
484
|
end
|
378
485
|
|
379
486
|
# Object that represents the summary of a Nexpose Site.
|
data/lib/nexpose/tag.rb
ADDED
@@ -0,0 +1,343 @@
|
|
1
|
+
module Nexpose
|
2
|
+
module_function
|
3
|
+
|
4
|
+
class Connection
|
5
|
+
# Lists all tags
|
6
|
+
#
|
7
|
+
# @return [Array[TagSummary]] List of current tags.
|
8
|
+
#
|
9
|
+
def list_tags
|
10
|
+
tag_summary = []
|
11
|
+
tags = JSON.parse(AJAX.get(self, '/api/2.0/tags'))
|
12
|
+
tags['resources'].each do |json|
|
13
|
+
tag_summary << TagSummary.parse(json)
|
14
|
+
end
|
15
|
+
tag_summary
|
16
|
+
end
|
17
|
+
alias_method :tags, :list_tags
|
18
|
+
|
19
|
+
# Deletes a tag by ID
|
20
|
+
#
|
21
|
+
# @param [Fixnum] tag_id ID of tag to delete
|
22
|
+
#
|
23
|
+
def delete_tag(tag_id)
|
24
|
+
AJAX.delete(self, "/api/2.0/tags/#{tag_id}")
|
25
|
+
end
|
26
|
+
|
27
|
+
# Lists all the tags on an asset
|
28
|
+
#
|
29
|
+
# @param [Fixnum] asset_id of the asset to list the applied tags for
|
30
|
+
# @return [Array[TagSummary]] list of tags on asset
|
31
|
+
#
|
32
|
+
def list_asset_tags(asset_id)
|
33
|
+
tag_summary = []
|
34
|
+
asset_tag = JSON.parse(AJAX.get(self, "/api/2.0/assets/#{asset_id}/tags"))
|
35
|
+
asset_tag['resources'].select { |r| r['asset_ids'].find { |i| i == asset_id } }.each do |json|
|
36
|
+
tag_summary << TagSummary.parse(json)
|
37
|
+
end
|
38
|
+
tag_summary
|
39
|
+
end
|
40
|
+
alias_method :asset_tags, :list_asset_tags
|
41
|
+
|
42
|
+
# Removes a tag from an asset
|
43
|
+
#
|
44
|
+
# @param [Fixnum] asset_id on which to remove tag
|
45
|
+
# @param [Fixnum] tag_id to remove from asset
|
46
|
+
#
|
47
|
+
def remove_tag_from_asset(asset_id, tag_id)
|
48
|
+
AJAX.delete(self, "/api/2.0/assets/#{asset_id}/tags/#{tag_id}")
|
49
|
+
end
|
50
|
+
|
51
|
+
# Lists all the tags on a site
|
52
|
+
#
|
53
|
+
# @param [Fixnum] site_id id of the site to get the applied tags
|
54
|
+
# @return [Array[TagSummary]] list of tags on site
|
55
|
+
#
|
56
|
+
def list_site_tags(site_id)
|
57
|
+
tag_summary = []
|
58
|
+
site_tag = JSON.parse(AJAX.get(self, "/api/2.0/sites/#{site_id}/tags"))
|
59
|
+
site_tag['resources'].each do |json|
|
60
|
+
tag_summary << TagSummary.parse(json)
|
61
|
+
end
|
62
|
+
tag_summary
|
63
|
+
end
|
64
|
+
|
65
|
+
# Removes a tag from a site
|
66
|
+
#
|
67
|
+
# @param [Fixnum] site_id id of the site on which to remove the tag
|
68
|
+
# @param [Fixnum] tag_id id of the tag to remove
|
69
|
+
#
|
70
|
+
def remove_tag_from_site(site_id, tag_id)
|
71
|
+
AJAX.delete(self, "/api/2.0/sites/#{site_id}/tags/#{tag_id}")
|
72
|
+
end
|
73
|
+
|
74
|
+
# Lists all the tags on an asset_group
|
75
|
+
#
|
76
|
+
# @param [Fixnum] asset_group_id id of the group on which tags are listed
|
77
|
+
# @return [Array[TagSummary]] list of tags on asset group
|
78
|
+
#
|
79
|
+
def list_asset_group_tags(asset_group_id)
|
80
|
+
tag_summary = []
|
81
|
+
asset_group_tag = JSON.parse(AJAX.get(self, "/api/2.0/asset_groups/#{asset_group_id}/tags"))
|
82
|
+
asset_group_tag['resources'].each do |json|
|
83
|
+
tag_summary << TagSummary.parse(json)
|
84
|
+
end
|
85
|
+
tag_summary
|
86
|
+
end
|
87
|
+
alias_method :group_tags, :list_asset_group_tags
|
88
|
+
alias_method :asset_group_tags, :list_asset_group_tags
|
89
|
+
|
90
|
+
# Removes a tag from an asset_group
|
91
|
+
#
|
92
|
+
# @param [Fixnum] asset_group_id id of group on which to remove tag
|
93
|
+
# @param [Fixnum] tag_id of the tag to remove from asset group
|
94
|
+
#
|
95
|
+
def remove_tag_from_asset_group(asset_group_id, tag_id)
|
96
|
+
AJAX.delete(self, "/api/2.0/asset_groups/#{asset_group_id}/tags/#{tag_id}")
|
97
|
+
end
|
98
|
+
alias_method :remove_tag_from_group, :remove_tag_from_asset_group
|
99
|
+
|
100
|
+
# Returns the criticality value which takes precedent for an asset
|
101
|
+
#
|
102
|
+
# @param [Fixnum] asset_id id of asset on which criticality tag is selected
|
103
|
+
# @return [String] selected_criticality string of the relevant criticality; nil if not tagged
|
104
|
+
#
|
105
|
+
def selected_criticality_tag(asset_id)
|
106
|
+
selected_criticality = AJAX.get(self, "/data/asset/#{asset_id}/selected-criticality-tag")
|
107
|
+
selected_criticality.empty? ? nil : JSON.parse(selected_criticality)['name']
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Summary value object for tag information
|
112
|
+
#
|
113
|
+
class TagSummary
|
114
|
+
|
115
|
+
# ID of tag
|
116
|
+
attr_accessor :id
|
117
|
+
|
118
|
+
# Name of tag
|
119
|
+
attr_accessor :name
|
120
|
+
|
121
|
+
# One of Tag::Type::Generic
|
122
|
+
attr_accessor :type
|
123
|
+
|
124
|
+
def initialize(name, type, id)
|
125
|
+
@name, @type, @id = name, type, id
|
126
|
+
end
|
127
|
+
|
128
|
+
def self.parse(json)
|
129
|
+
new(json['tag_name'], json['tag_type'], json['tag_id'])
|
130
|
+
end
|
131
|
+
|
132
|
+
def self.parse_xml(xml)
|
133
|
+
new(xml.attributes['name'], xml.attributes['type'], xml.attributes['id'].to_i)
|
134
|
+
end
|
135
|
+
|
136
|
+
# XML representation of the tag summary as required by Site and AssetGroup
|
137
|
+
#
|
138
|
+
# @return [ELEMENT] XML element
|
139
|
+
|
140
|
+
def as_xml
|
141
|
+
xml = REXML::Element.new('Tag')
|
142
|
+
xml.add_attribute('id', @id)
|
143
|
+
xml.add_attribute('name', @name)
|
144
|
+
xml.add_attribute('type', @type)
|
145
|
+
xml
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# Tag object containing tag details
|
150
|
+
#
|
151
|
+
class Tag < TagSummary
|
152
|
+
module Type
|
153
|
+
# Criticality tag types
|
154
|
+
module Level
|
155
|
+
VERY_HIGH = 'Very High'
|
156
|
+
HIGH = 'High'
|
157
|
+
MEDIUM = 'Medium'
|
158
|
+
LOW = 'Low'
|
159
|
+
VERY_LOW = 'Very Low'
|
160
|
+
end
|
161
|
+
|
162
|
+
# Tag types
|
163
|
+
module Generic
|
164
|
+
CUSTOM = 'CUSTOM'
|
165
|
+
OWNER = 'OWNER'
|
166
|
+
LOCATION = 'LOCATION'
|
167
|
+
CRITICALITY = 'CRITICALITY'
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
DEFAULT_COLOR = '#F6F6F6'
|
172
|
+
|
173
|
+
# Creation source
|
174
|
+
attr_accessor :source
|
175
|
+
|
176
|
+
# HEX color code of tag
|
177
|
+
attr_accessor :color
|
178
|
+
|
179
|
+
# Risk modifier
|
180
|
+
attr_accessor :risk_modifier
|
181
|
+
|
182
|
+
# Array containing Site IDs to be associated with tag
|
183
|
+
attr_accessor :site_ids
|
184
|
+
|
185
|
+
# Array containing Asset IDs to be associated with tag
|
186
|
+
attr_accessor :asset_ids
|
187
|
+
|
188
|
+
# Array containing Asset IDs directly associated with the tag
|
189
|
+
attr_accessor :associated_asset_ids
|
190
|
+
|
191
|
+
# Array containing Asset Group IDs to be associated with tag
|
192
|
+
attr_accessor :asset_group_ids
|
193
|
+
alias_method :group_ids, :asset_group_ids
|
194
|
+
alias_method :group_ids=, :asset_group_ids=
|
195
|
+
|
196
|
+
# A TagCriteria
|
197
|
+
attr_accessor :search_criteria
|
198
|
+
|
199
|
+
def initialize(name, type, id = -1)
|
200
|
+
@name, @type, @id = name, type, id
|
201
|
+
@source = 'nexpose-client'
|
202
|
+
@color = @type == Type::Generic::CUSTOM ? DEFAULT_COLOR : nil
|
203
|
+
end
|
204
|
+
|
205
|
+
# Creates and saves a tag to Nexpose console
|
206
|
+
#
|
207
|
+
# @param [Connection] connection Nexpose connection
|
208
|
+
# @return [Fixnum] ID of saved tag
|
209
|
+
#
|
210
|
+
def save(connection)
|
211
|
+
params = to_json
|
212
|
+
if @id == -1
|
213
|
+
uri = AJAX.post(connection, '/api/2.0/tags', params, AJAX::CONTENT_TYPE::JSON)
|
214
|
+
@id = uri.split('/').last.to_i
|
215
|
+
else
|
216
|
+
AJAX.put(connection, "/api/2.0/tags/#{@id}", params, AJAX::CONTENT_TYPE::JSON)
|
217
|
+
end
|
218
|
+
@id
|
219
|
+
end
|
220
|
+
|
221
|
+
# Retrieve detailed description of a single tag
|
222
|
+
#
|
223
|
+
# @param [Connection] connection Nexpose connection
|
224
|
+
# @param [Fixnum] ID of tag to retrieve
|
225
|
+
# @return [Tag] requested tag
|
226
|
+
#
|
227
|
+
def self.load(connection, tag_id)
|
228
|
+
json = JSON.parse(AJAX.get(connection, "/api/2.0/tags/#{tag_id}"))
|
229
|
+
Tag.parse(json)
|
230
|
+
end
|
231
|
+
|
232
|
+
def to_json
|
233
|
+
json = {
|
234
|
+
'tag_name' => @name,
|
235
|
+
'tag_type' => @type,
|
236
|
+
'tag_id' => @id,
|
237
|
+
'attributes' => [
|
238
|
+
{ 'tag_attribute_name' => 'SOURCE',
|
239
|
+
'tag_attribute_value' => @source }
|
240
|
+
],
|
241
|
+
'tag_config' => { 'site_ids' => @site_ids,
|
242
|
+
'tag_associated_asset_ids' => @associated_asset_ids,
|
243
|
+
'asset_group_ids' => @asset_group_ids,
|
244
|
+
'search_criteria' => @search_criteria ? @search_criteria.to_map : nil
|
245
|
+
}
|
246
|
+
}
|
247
|
+
if @type == Type::Generic::CUSTOM
|
248
|
+
json['attributes'] << { 'tag_attribute_name' => 'COLOR', 'tag_attribute_value' => @color }
|
249
|
+
end
|
250
|
+
JSON.generate(json)
|
251
|
+
end
|
252
|
+
|
253
|
+
# Delete this tag from Nexpose console
|
254
|
+
#
|
255
|
+
# @param [Connection] connection Nexpose connection
|
256
|
+
#
|
257
|
+
def delete(connection)
|
258
|
+
connection.delete_tag(@id)
|
259
|
+
end
|
260
|
+
|
261
|
+
def self.parse(json)
|
262
|
+
color = json['attributes'].find { |attr| attr['tag_attribute_name'] == 'COLOR' }
|
263
|
+
color = color['tag_attribute_value'] if color
|
264
|
+
source = json['attributes'].find { |attr| attr['tag_attribute_name'] == 'SOURCE' }
|
265
|
+
source = source['tag_attribute_value'] if source
|
266
|
+
tag = Tag.new(json['tag_name'], json['tag_type'], json['tag_id'])
|
267
|
+
tag.color = color
|
268
|
+
tag.source = source
|
269
|
+
tag.asset_ids = json['asset_ids']
|
270
|
+
if json['tag_config']
|
271
|
+
tag.site_ids = json['tag_config']['site_ids']
|
272
|
+
tag.associated_asset_ids = json['tag_config']['tag_associated_asset_ids']
|
273
|
+
tag.asset_group_ids = json['tag_config']['asset_group_ids']
|
274
|
+
criteria = json['tag_config']['search_criteria']
|
275
|
+
tag.search_criteria = criteria ? Criteria.parse(criteria) : nil
|
276
|
+
end
|
277
|
+
modifier = json['attributes'].find { |attr| attr['tag_attribute_name'] == 'RISK_MODIFIER' }
|
278
|
+
if modifier
|
279
|
+
tag.risk_modifier = modifier['tag_attribute_value'].to_i
|
280
|
+
end
|
281
|
+
tag
|
282
|
+
end
|
283
|
+
|
284
|
+
# Adds a tag to an asset
|
285
|
+
#
|
286
|
+
# @param [Connection] connection Nexpose connection
|
287
|
+
# @param [Fixnum] asset_id of the asset to be tagged
|
288
|
+
# @return [Fixnum] ID of applied tag
|
289
|
+
#
|
290
|
+
def add_to_asset(connection, asset_id)
|
291
|
+
params = _to_json_for_add
|
292
|
+
uri = AJAX.post(connection, "/api/2.0/assets/#{asset_id}/tags", params, AJAX::CONTENT_TYPE::JSON)
|
293
|
+
@id = uri.split('/').last.to_i
|
294
|
+
end
|
295
|
+
|
296
|
+
# Adds a tag to a site
|
297
|
+
#
|
298
|
+
# @param [Connection] connection Nexpose connection
|
299
|
+
# @param [Fixnum] site_id of the site to be tagged
|
300
|
+
# @return [Fixnum] ID of applied tag
|
301
|
+
#
|
302
|
+
def add_to_site(connection, site_id)
|
303
|
+
params = _to_json_for_add
|
304
|
+
uri = AJAX.post(connection, "/api/2.0/sites/#{site_id}/tags", params, AJAX::CONTENT_TYPE::JSON)
|
305
|
+
@id = uri.split('/').last.to_i
|
306
|
+
end
|
307
|
+
|
308
|
+
# Adds a tag to an asset group
|
309
|
+
#
|
310
|
+
# @param [Connection] connection Nexpose connection
|
311
|
+
# @param [Fixnum] group_id id of the asset group to be tagged
|
312
|
+
# @return [Fixnum] ID of applied tag
|
313
|
+
#
|
314
|
+
def add_to_group(connection, group_id)
|
315
|
+
params = _to_json_for_add
|
316
|
+
uri = AJAX.post(connection, "/api/2.0/asset_groups/#{group_id}/tags", params, AJAX::CONTENT_TYPE::JSON)
|
317
|
+
@id = uri.split('/').last.to_i
|
318
|
+
end
|
319
|
+
alias_method :add_to_asset_group, :add_to_group
|
320
|
+
|
321
|
+
private
|
322
|
+
|
323
|
+
def _to_json_for_add
|
324
|
+
if @id == -1
|
325
|
+
json = {
|
326
|
+
'tag_name' => @name,
|
327
|
+
'tag_type' => @type,
|
328
|
+
'attributes' => [
|
329
|
+
{ 'tag_attribute_name' => 'SOURCE',
|
330
|
+
'tag_attribute_value' => @source }
|
331
|
+
],
|
332
|
+
}
|
333
|
+
if @type == Tag::Type::Generic::CUSTOM
|
334
|
+
json['attributes'] << { 'tag_attribute_name' => 'COLOR', 'tag_attribute_value' => @color }
|
335
|
+
end
|
336
|
+
params = JSON.generate(json)
|
337
|
+
else
|
338
|
+
params = JSON.generate('tag_id' => @id)
|
339
|
+
end
|
340
|
+
params
|
341
|
+
end
|
342
|
+
end
|
343
|
+
end
|