foreman_openscap 0.6.3 → 0.6.4

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.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -0
  3. data/app/assets/javascripts/foreman_openscap/openscap_proxy.js +7 -0
  4. data/app/assets/javascripts/foreman_openscap/policy_edit.js +15 -0
  5. data/app/controllers/api/v2/compliance/arf_reports_controller.rb +2 -2
  6. data/app/controllers/api/v2/compliance/policies_controller.rb +16 -4
  7. data/app/controllers/api/v2/compliance/scap_contents_controller.rb +2 -2
  8. data/app/controllers/api/v2/compliance/tailoring_files_controller.rb +92 -0
  9. data/app/controllers/concerns/foreman/controller/parameters/policy_api.rb +2 -2
  10. data/app/controllers/concerns/foreman/controller/parameters/tailoring_file.rb +15 -0
  11. data/app/controllers/openscap_proxies_controller.rb +31 -0
  12. data/app/controllers/policies_controller.rb +14 -15
  13. data/app/controllers/scap_contents_controller.rb +0 -10
  14. data/app/controllers/tailoring_files_controller.rb +75 -0
  15. data/app/helpers/compliance_dashboard_helper.rb +2 -2
  16. data/app/helpers/policies_helper.rb +29 -1
  17. data/app/helpers/tailoring_files_helper.rb +5 -0
  18. data/app/lib/proxy_api/openscap.rb +18 -2
  19. data/app/models/concerns/foreman_openscap/data_stream_content.rb +43 -0
  20. data/app/models/concerns/foreman_openscap/host_extensions.rb +1 -1
  21. data/app/models/concerns/foreman_openscap/hostgroup_extensions.rb +8 -0
  22. data/app/models/foreman_openscap/policy.rb +28 -3
  23. data/app/models/foreman_openscap/scap_content.rb +4 -72
  24. data/app/models/foreman_openscap/scap_content_profile.rb +2 -0
  25. data/app/models/foreman_openscap/tailoring_file.rb +19 -0
  26. data/app/services/foreman_openscap/openscap_proxy_version_check.rb +63 -0
  27. data/app/validators/foreman_openscap/data_stream_validator.rb +44 -0
  28. data/app/views/api/v2/compliance/policies/base.json.rabl +2 -1
  29. data/app/views/api/v2/compliance/tailoring_files/base.json.rabl +6 -0
  30. data/app/views/api/v2/compliance/tailoring_files/index.json.rabl +3 -0
  31. data/app/views/api/v2/compliance/tailoring_files/main.json.rabl +5 -0
  32. data/app/views/api/v2/compliance/tailoring_files/show.json.rabl +7 -0
  33. data/app/views/arf_reports/_list.html.erb +3 -2
  34. data/app/views/dashboard/_compliance_host_reports_widget.html.erb +3 -3
  35. data/app/views/policies/_form.html.erb +9 -0
  36. data/app/views/policies/_list.html.erb +16 -4
  37. data/app/views/policies/_tailoring_file_selected.html.erb +3 -0
  38. data/app/views/policies/steps/_scap_content_form.html.erb +8 -0
  39. data/app/views/policies/welcome.html.erb +12 -13
  40. data/app/views/scap_contents/_list.html.erb +1 -1
  41. data/app/views/scap_contents/welcome.html.erb +14 -13
  42. data/app/views/smart_proxies/_openscap_spool.html.erb +9 -0
  43. data/app/views/smart_proxies/plugins/_openscap.html.erb +12 -0
  44. data/app/views/tailoring_files/_form.html.erb +25 -0
  45. data/app/views/tailoring_files/_list.html.erb +29 -0
  46. data/app/views/tailoring_files/edit.html.erb +3 -0
  47. data/app/views/tailoring_files/index.html.erb +3 -0
  48. data/app/views/tailoring_files/new.html.erb +3 -0
  49. data/app/views/tailoring_files/welcome.html.erb +21 -0
  50. data/config/routes.rb +22 -0
  51. data/db/migrate/20161109155255_create_tailoring_files.rb +23 -0
  52. data/db/migrate/20161223153249_add_permissions_to_arf_report.rb +11 -0
  53. data/lib/foreman_openscap/engine.rb +30 -5
  54. data/lib/foreman_openscap/version.rb +1 -1
  55. data/test/factories/policy_factory.rb +2 -0
  56. data/test/factories/scap_content_related.rb +7 -0
  57. data/test/files/tailoring_files/ssg-firefox-ds-tailoring-2.xml +23 -0
  58. data/test/files/tailoring_files/ssg-firefox-ds-tailoring.xml +31 -0
  59. data/test/functional/api/v2/compliance/policies_controller_test.rb +35 -8
  60. data/test/functional/api/v2/compliance/scap_contents_controller_test.rb +1 -1
  61. data/test/functional/api/v2/compliance/tailoring_files_controller_test.rb +63 -0
  62. data/test/functional/openscap_proxies_controller_test.rb +14 -0
  63. data/test/functional/tailoring_files_controller_test.rb +38 -0
  64. data/test/test_plugin_helper.rb +18 -24
  65. data/test/unit/openscap_host_test.rb +11 -1
  66. data/test/unit/policy_test.rb +26 -0
  67. data/test/unit/services/tailoring_files_proxy_check_test.rb +27 -0
  68. data/test/unit/tailoring_file_test.rb +26 -0
  69. metadata +59 -20
@@ -1,8 +1,8 @@
1
1
  module ComplianceDashboardHelper
2
2
 
3
3
  def latest_compliance_headers
4
- string = "<th>#{_("Host")}</th>"
5
- string += "<th>#{_("Policy")}</th>"
4
+ string = "<th class='col-md-7'>#{_("Host")}</th>"
5
+ string += "<th class='col-md-3'>#{_("Policy")}</th>"
6
6
  # TRANSLATORS: initial character of Passed
7
7
  string += translated_header(s_('Passed|P'), _('Passed'))
8
8
  # TRANSLATORS: initial character of Failed
@@ -5,8 +5,16 @@ module PoliciesHelper
5
5
  return []
6
6
  end
7
7
 
8
+ def policy_profile_from_scap_content(policy)
9
+ policy.scap_content_profile.nil? ? "Default" : policy.scap_content_profile.title
10
+ end
11
+
12
+ def effective_policy_profile(policy)
13
+ policy.tailoring_file ? policy.tailoring_file_profile.title : policy_profile_from_scap_content(policy)
14
+ end
15
+
8
16
  def scap_content_selector(form)
9
- scap_contents = ::ForemanOpenscap::ScapContent.all
17
+ scap_contents = ::ForemanOpenscap::ScapContent.authorized(:view_scap_contents).all
10
18
  if scap_contents.length > 1
11
19
  select_f form, :scap_content_id, scap_contents, :id, :title,
12
20
  {:include_blank => _("Choose existing SCAP Content")},
@@ -38,6 +46,26 @@ module PoliciesHelper
38
46
  end
39
47
  end
40
48
 
49
+ def tailoring_file_selector(form)
50
+ select_f form, :tailoring_file_id, ForemanOpenscap::TailoringFile.all.authorized(:view_tailoring_files), :id, :name,
51
+ { :include_blank => _('Choose Tailoring File') },
52
+ { :label => _('Tailoring File'),
53
+ :onchange => 'tailoring_file_selected(this)',
54
+ :'data-url' => method_path('tailoring_file_selected') }
55
+ end
56
+
57
+ def tailoring_file_profile_selector(form, tailoring_file)
58
+ if tailoring_file
59
+ select_f form, :tailoring_file_profile_id, tailoring_file.scap_content_profiles, :id, :title,
60
+ { :selected => tailoring_file.scap_content_profiles.first.id },
61
+ { :label => _("XCCDF Profile in Tailoring File"),
62
+ :help_inline => _("This profile will be used to override the one from scap content") }
63
+ else
64
+ # to make sure tailoring profile id is nil when tailoring file is deselected
65
+ form.hidden_field(:tailoring_file_profile_id, :value => nil)
66
+ end
67
+ end
68
+
41
69
  def submit_or_cancel_policy(form, overwrite = nil, args = { })
42
70
  args[:cancel_path] ||= send("#{controller_name}_path")
43
71
  content_tag(:div, :class => "clearfix") do
@@ -0,0 +1,5 @@
1
+ module TailoringFilesHelper
2
+ def run_tailoring_proxy_check
3
+ ForemanOpenscap::OpenscapProxyVersionCheck.new.run
4
+ end
5
+ end
@@ -4,14 +4,24 @@ module ::ProxyAPI
4
4
  @url = args[:url] + '/compliance/'
5
5
  super args
6
6
  @connect_params[:headers].merge!(:content_type => :xml)
7
+ @connect_params[:timeout] = timeout
7
8
  end
8
9
 
9
10
  def fetch_policies_for_scap_content(scap_file)
10
11
  parse(post(scap_file, "scap_content/policies"))
11
12
  end
12
13
 
13
- def validate_scap_content(scap_file)
14
- parse(post(scap_file, "scap_content/validator"))
14
+ def fetch_profiles_for_tailoring_file(scap_file)
15
+ parse(post(scap_file, "tailoring_file/profiles"))
16
+ end
17
+
18
+ def validate_scap_file(scap_file, type)
19
+ parse(post(scap_file, "scap_file/validator/#{type}"))
20
+ rescue RestClient::RequestTimeout => e
21
+ raise ::ProxyAPI::ProxyException.new(url, e, N_("Request timed out. Please try increasing Settings -> proxy_request_timeout"))
22
+ rescue RestClient::ResourceNotFound => e
23
+ raise ::ProxyAPI::ProxyException.new(url, e,
24
+ N_("Could not validate %s. Please make sure you have appropriate proxy version to use this functionality") % scap_file.class)
15
25
  end
16
26
 
17
27
  def policy_html_guide(scap_file, policy)
@@ -46,5 +56,11 @@ module ::ProxyAPI
46
56
  false
47
57
  end
48
58
  end
59
+
60
+ private
61
+
62
+ def timeout
63
+ Setting[:proxy_request_timeout] && Setting[:proxy_request_timeout] > 120 ? Setting[:proxy_request_timeout] : 120
64
+ end
49
65
  end
50
66
  end
@@ -0,0 +1,43 @@
1
+ module ForemanOpenscap
2
+ module DataStreamContent
3
+ require 'digest/sha2'
4
+
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ validates :digest, :presence => true
9
+ validates :scap_file, :presence => true
10
+
11
+ validates_with ForemanOpenscap::DataStreamValidator
12
+
13
+ after_save :create_profiles
14
+
15
+ before_validation :redigest, :if => lambda { |ds_content| ds_content.persisted? && ds_content.scap_file_changed? }
16
+ before_destroy ActiveRecord::Base::EnsureNotUsedBy.new(:policies)
17
+ end
18
+
19
+ def proxy_url
20
+ @proxy_url ||= SmartProxy.with_features('Openscap').find do |proxy|
21
+ available = ProxyAPI::AvailableProxy.new(:url => proxy.url)
22
+ available.available?
23
+ end.try(:url)
24
+ @proxy_url
25
+ end
26
+
27
+ def digest
28
+ self[:digest] ||= Digest::SHA256.hexdigest(scap_file.to_s)
29
+ end
30
+
31
+ private
32
+
33
+ def redigest
34
+ self[:digest] = Digest::SHA256.hexdigest(scap_file.to_s)
35
+ end
36
+
37
+ def create_profiles
38
+ fetch_profiles.each do |key, title|
39
+ ScapContentProfile.where(:profile_id => key, :title => title, "#{self.class.to_s.demodulize.underscore}_id".to_sym => id).first_or_create
40
+ end
41
+ end
42
+ end
43
+ end
@@ -67,7 +67,7 @@ module ForemanOpenscap
67
67
  end
68
68
 
69
69
  def combined_policies
70
- combined = self.hostgroup ? self.policies + self.hostgroup.policies : self.policies
70
+ combined = self.hostgroup ? self.policies + self.hostgroup.policies + self.hostgroup.inherited_policies : self.policies
71
71
  combined.uniq
72
72
  end
73
73
 
@@ -8,6 +8,14 @@ module ForemanOpenscap
8
8
  has_many :policies, :through => :asset_policies, :class_name => "::ForemanOpenscap::Policy"
9
9
  end
10
10
 
11
+ def inherited_policies
12
+ return [] unless parent
13
+
14
+ ancestors.inject([]) do |policies, hostgroup|
15
+ policies += hostgroup.policies
16
+ end.uniq
17
+ end
18
+
11
19
  unless defined?(Katello::System)
12
20
  private
13
21
 
@@ -6,6 +6,8 @@ module ForemanOpenscap
6
6
 
7
7
  belongs_to :scap_content
8
8
  belongs_to :scap_content_profile
9
+ belongs_to :tailoring_file
10
+ belongs_to :tailoring_file_profile, :class_name => ForemanOpenscap::ScapContentProfile
9
11
  has_many :policy_arf_reports
10
12
  has_many :arf_reports, :through => :policy_arf_reports, :dependent => :destroy
11
13
  has_many :asset_policies
@@ -28,7 +30,7 @@ module ForemanOpenscap
28
30
  validates :scap_content_id, presence: true, if: Proc.new { |policy| policy.should_validate?('SCAP Content') }
29
31
  validates :scap_content_profile_id, presence: true, if: Proc.new { |policy| policy.should_validate?('SCAP Content') }
30
32
 
31
- validate :valid_cron_line, :valid_weekday, :valid_day_of_month
33
+ validate :valid_cron_line, :valid_weekday, :valid_day_of_month, :valid_tailoring, :valid_tailoring_profile
32
34
 
33
35
  after_save :assign_policy_to_hostgroups
34
36
  # before_destroy - ensure that the policy has no hostgroups, or classes
@@ -166,9 +168,11 @@ module ForemanOpenscap
166
168
  def to_enc
167
169
  {
168
170
  'id' => self.id,
169
- 'profile_id' => self.scap_content_profile.try(:profile_id) || '',
171
+ 'profile_id' => profile_for_scan,
170
172
  'content_path' => "/var/lib/openscap/content/#{self.scap_content.digest}.xml",
171
- 'download_path' => "/compliance/policies/#{self.id}/content" # default to proxy path
173
+ 'tailoring_path' => tailoring_file ? "/var/lib/openscap/tailoring/#{self.tailoring_file.digest}.xml" : '',
174
+ 'download_path' => "/compliance/policies/#{self.id}/content", # default to proxy path
175
+ 'tailoring_download_path' => "/compliance/policies/#{self.id}/tailoring"
172
176
  }.merge(period_enc)
173
177
  end
174
178
 
@@ -273,6 +277,17 @@ module ForemanOpenscap
273
277
  end
274
278
  end
275
279
 
280
+ def valid_tailoring
281
+ errors.add(:tailoring_file_id, _("must be present when tailoring file profile present")) if tailoring_file_profile_id && !tailoring_file_id
282
+ errors.add(:tailoring_file_profile_id, _("must be present when tailoring file present")) if !tailoring_file_profile_id && tailoring_file_id
283
+ end
284
+
285
+ def valid_tailoring_profile
286
+ if tailoring_file && tailoring_file_profile && !ScapContentProfile.where(:tailoring_file_id => tailoring_file_id).include?(tailoring_file_profile)
287
+ errors.add(:tailoring_file_profile, _("does not come from selected tailoring file"))
288
+ end
289
+ end
290
+
276
291
  def assign_policy_to_hostgroups
277
292
  if hostgroups.any?
278
293
  puppetclass = find_scap_puppetclass
@@ -283,6 +298,16 @@ module ForemanOpenscap
283
298
  end
284
299
  end
285
300
 
301
+ def profile_for_scan
302
+ if tailoring_file_profile
303
+ tailoring_file_profile.profile_id
304
+ elsif scap_content_profile
305
+ scap_content_profile.profile_id
306
+ else
307
+ ''
308
+ end
309
+ end
310
+
286
311
  def find_scap_puppetclass
287
312
  Puppetclass.find_by_name(SCAP_PUPPET_CLASS)
288
313
  end
@@ -1,56 +1,13 @@
1
- require 'digest/sha2'
2
-
3
1
  module ForemanOpenscap
4
- class DataStreamValidator < ActiveModel::Validator
5
- def validate(scap_content)
6
- return unless scap_content.scap_file_changed?
7
-
8
- unless SmartProxy.with_features('Openscap').any?
9
- scap_content.errors.add(:base, _('No proxy with OpenSCAP features'))
10
- return false
11
- end
12
-
13
- if scap_content.proxy_url.nil?
14
- scap_content.errors.add(:base, _('No available proxy to validate SCAP content'))
15
- return false
16
- end
17
-
18
- begin
19
- api = ProxyAPI::Openscap.new(:url => scap_content.proxy_url)
20
- errors = api.validate_scap_content(scap_content.scap_file)
21
- if errors && errors['errors'].any?
22
- errors['errors'].each { |error| scap_content.errors.add(:scap_file, _(error)) }
23
- return false
24
- end
25
- rescue *ProxyAPI::AvailableProxy::HTTP_ERRORS => e
26
- scap_content.errors.add(:base, _('No available proxy to validate. Returned with error: %s') % e)
27
- return false
28
- end
29
-
30
-
31
- unless (scap_content.scap_content_profiles.map(&:profile_id) - scap_content.fetch_profiles.keys).empty?
32
- scap_content.errors.add(:scap_file, _('Changed file does not include existing SCAP content profiles'))
33
- return false
34
- end
35
- end
36
- end
37
-
38
2
  class ScapContent < ActiveRecord::Base
39
3
  include Authorizable
40
4
  include Taxonomix
5
+ include DataStreamContent
41
6
 
42
7
  has_many :scap_content_profiles, :dependent => :destroy
43
8
  has_many :policies
44
9
 
45
- before_destroy EnsureNotUsedBy.new(:policies)
46
-
47
- validates_with DataStreamValidator
48
10
  validates :title, :presence => true, :length => { :maximum => 255 }
49
- validates :digest, :presence => true
50
- validates :scap_file, :presence => true
51
-
52
- after_save :create_profiles
53
- before_validation :redigest, :if => lambda { |scap_content| scap_content.persisted? && scap_content.scap_file_changed? }
54
11
 
55
12
  scoped_search :on => :title, :complete_value => true
56
13
  scoped_search :on => :original_filename, :complete_value => true, :rename => :filename
@@ -77,8 +34,9 @@ module ForemanOpenscap
77
34
  title
78
35
  end
79
36
 
80
- def digest
81
- self[:digest] ||= Digest::SHA256.hexdigest "#{scap_file}"
37
+ def as_json(*args)
38
+ hash = super
39
+ hash["scap_file"] = scap_file.to_s.encode('utf-8', :invalid => :replace, :undef => :replace, :replace => '_')
82
40
  end
83
41
 
84
42
  def fetch_profiles
@@ -86,31 +44,5 @@ module ForemanOpenscap
86
44
  profiles = api.fetch_policies_for_scap_content(scap_file)
87
45
  profiles
88
46
  end
89
-
90
- def proxy_url
91
- @proxy_url ||= SmartProxy.with_features('Openscap').find do |proxy|
92
- available = ProxyAPI::AvailableProxy.new(:url => proxy.url)
93
- available.available?
94
- end.try(:url)
95
- @proxy_url
96
- end
97
-
98
- def as_json(*args)
99
- hash = super
100
- hash["scap_file"] = scap_file.to_s.encode('utf-8', :invalid => :replace, :undef => :replace, :replace => '_')
101
- end
102
-
103
- private
104
-
105
- def create_profiles
106
- profiles = fetch_profiles
107
- profiles.each {|key, title|
108
- scap_content_profiles.where(:profile_id => key, :title => title).first_or_create
109
- }
110
- end
111
-
112
- def redigest
113
- self[:digest] = Digest::SHA256.hexdigest "#{scap_file}"
114
- end
115
47
  end
116
48
  end
@@ -2,5 +2,7 @@ module ForemanOpenscap
2
2
  class ScapContentProfile < ActiveRecord::Base
3
3
  belongs_to :scap_content
4
4
  has_many :policies
5
+ belongs_to :tailoring_file
6
+ has_many :tailoring_file_policies, :class_name => ForemanOpenscap::Policy
5
7
  end
6
8
  end
@@ -0,0 +1,19 @@
1
+ module ForemanOpenscap
2
+ class TailoringFile < ActiveRecord::Base
3
+ include Authorizable
4
+ include Taxonomix
5
+ include DataStreamContent
6
+
7
+ has_many :policies
8
+ has_many :scap_content_profiles, :dependent => :destroy
9
+ validates :name, :presence => true, :uniqueness => true, :length => { :maximum => 255 }
10
+
11
+ scoped_search :on => :name, :complete_value => true
12
+ scoped_search :on => :original_filename, :complete_value => true, :rename => :filename
13
+
14
+ def fetch_profiles
15
+ api = ProxyAPI::Openscap.new(:url => proxy_url)
16
+ api.fetch_profiles_for_tailoring_file(scap_file)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,63 @@
1
+ module ForemanOpenscap
2
+ class OpenscapProxyVersionCheck
3
+
4
+ def initialize
5
+ @versions = {}
6
+ @message = ''
7
+ @down = []
8
+ end
9
+
10
+ def run
11
+ @versions = openscap_proxy_versions.select do |key, value|
12
+ Gem::Version.new(value) <= Gem::Version.new("0.6.1")
13
+ end
14
+ self
15
+ end
16
+
17
+ def pass?
18
+ !any_outdated? && !any_unreachable?
19
+ end
20
+
21
+ def any_outdated?
22
+ !@versions.empty?
23
+ end
24
+
25
+ def any_unreachable?
26
+ !@down.empty?
27
+ end
28
+
29
+ def message
30
+ if pass?
31
+ @message
32
+ else
33
+ build_message
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def build_message
40
+ @message = _('This feature is temporarily disabled. ')
41
+ @message << _('The following Smart Proxies need to be updated to unlock the feature: %s. ') % @versions.keys.to_sentence if any_outdated?
42
+ @message << _('The following proxies could not be reached: %s. Please make sure they are available so Foreman can check their versions.') % @down.to_sentence if any_unreachable?
43
+ @message
44
+ end
45
+
46
+ def get_openscap_proxies
47
+ SmartProxy.with_features "Openscap"
48
+ end
49
+
50
+ def openscap_proxy_versions
51
+ get_openscap_proxies.inject({}) do |memo, proxy|
52
+ begin
53
+ status = ProxyStatus::Version.new(proxy).version
54
+ openscap_version = status["modules"]["openscap"]
55
+ memo[proxy.name] = openscap_version
56
+ rescue Foreman::WrappedException
57
+ @down << proxy.name
58
+ end
59
+ memo
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,44 @@
1
+ module ForemanOpenscap
2
+ class DataStreamValidator < ActiveModel::Validator
3
+ def validate(data_stream_content)
4
+ return unless data_stream_content.scap_file_changed?
5
+
6
+ content_type = data_type(data_stream_content)
7
+
8
+ unless SmartProxy.with_features('Openscap').any?
9
+ data_stream_content.errors.add(:base, _('No proxy with OpenSCAP features'))
10
+ return false
11
+ end
12
+
13
+ if data_stream_content.proxy_url.nil?
14
+ data_stream_content.errors.add(:base, _('No available proxy to validate SCAP data stream file'))
15
+ return false
16
+ end
17
+
18
+ begin
19
+ api = ProxyAPI::Openscap.new(:url => data_stream_content.proxy_url)
20
+ errors = api.validate_scap_file(data_stream_content.scap_file, content_type)
21
+ if errors && errors['errors'].any?
22
+ errors['errors'].each { |error| data_stream_content.errors.add(:scap_file, _(error)) }
23
+ return false
24
+ end
25
+ rescue *ProxyAPI::AvailableProxy::HTTP_ERRORS => e
26
+ data_stream_content.errors.add(:base, _('No available proxy to validate. Returned with error: %s') % e)
27
+ return false
28
+ end
29
+
30
+ is_scap_content = content_type == 'scap_content'
31
+
32
+ if is_scap_content && !(data_stream_content.scap_content_profiles.map(&:profile_id) - data_stream_content.fetch_profiles.keys).empty?
33
+ data_stream_content.errors.add(:scap_file, _('Changed file does not include existing SCAP content profiles'))
34
+ return false
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def data_type(data_stream_content)
41
+ data_stream_content.class.to_s.demodulize.underscore
42
+ end
43
+ end
44
+ end