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.
@@ -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