nexpose 0.6.5 → 0.7.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/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
|