nexpose 0.6.5 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
- r = connection.execute('<SiteSaveRequest session-id="' + connection.session_id + '">' + to_xml + ' </SiteSaveRequest>')
231
- @id = r.attributes['site-id'].to_i if r.success
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.
@@ -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