zendesk_apps_support 4.29.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 (44) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +176 -0
  3. data/README.md +39 -0
  4. data/config/locales/en.yml +163 -0
  5. data/config/locales/translations/zendesk_apps_support.yml +405 -0
  6. data/lib/zendesk_apps_support.rb +42 -0
  7. data/lib/zendesk_apps_support/app_file.rb +44 -0
  8. data/lib/zendesk_apps_support/app_requirement.rb +11 -0
  9. data/lib/zendesk_apps_support/app_version.rb +78 -0
  10. data/lib/zendesk_apps_support/assets/default_app_logo.svg +10 -0
  11. data/lib/zendesk_apps_support/assets/default_styles.scss +3 -0
  12. data/lib/zendesk_apps_support/assets/default_template.html.erb +10 -0
  13. data/lib/zendesk_apps_support/assets/installed.js.erb +21 -0
  14. data/lib/zendesk_apps_support/assets/src.js.erb +37 -0
  15. data/lib/zendesk_apps_support/build_translation.rb +51 -0
  16. data/lib/zendesk_apps_support/engine.rb +11 -0
  17. data/lib/zendesk_apps_support/finders.rb +36 -0
  18. data/lib/zendesk_apps_support/i18n.rb +31 -0
  19. data/lib/zendesk_apps_support/installation.rb +22 -0
  20. data/lib/zendesk_apps_support/installed.rb +27 -0
  21. data/lib/zendesk_apps_support/location.rb +71 -0
  22. data/lib/zendesk_apps_support/manifest.rb +197 -0
  23. data/lib/zendesk_apps_support/manifest/location_options.rb +35 -0
  24. data/lib/zendesk_apps_support/manifest/no_override_hash.rb +50 -0
  25. data/lib/zendesk_apps_support/manifest/parameter.rb +23 -0
  26. data/lib/zendesk_apps_support/package.rb +402 -0
  27. data/lib/zendesk_apps_support/product.rb +31 -0
  28. data/lib/zendesk_apps_support/sass_functions.rb +43 -0
  29. data/lib/zendesk_apps_support/stylesheet_compiler.rb +38 -0
  30. data/lib/zendesk_apps_support/validations/banner.rb +35 -0
  31. data/lib/zendesk_apps_support/validations/manifest.rb +412 -0
  32. data/lib/zendesk_apps_support/validations/marketplace.rb +31 -0
  33. data/lib/zendesk_apps_support/validations/mime.rb +41 -0
  34. data/lib/zendesk_apps_support/validations/requests.rb +47 -0
  35. data/lib/zendesk_apps_support/validations/requirements.rb +154 -0
  36. data/lib/zendesk_apps_support/validations/secrets.rb +77 -0
  37. data/lib/zendesk_apps_support/validations/secure_settings.rb +37 -0
  38. data/lib/zendesk_apps_support/validations/source.rb +25 -0
  39. data/lib/zendesk_apps_support/validations/stylesheets.rb +28 -0
  40. data/lib/zendesk_apps_support/validations/svg.rb +81 -0
  41. data/lib/zendesk_apps_support/validations/templates.rb +20 -0
  42. data/lib/zendesk_apps_support/validations/translations.rb +160 -0
  43. data/lib/zendesk_apps_support/validations/validation_error.rb +77 -0
  44. metadata +327 -0
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ZendeskAppsSupport
4
+ module Validations
5
+ module Marketplace
6
+ WHITELISTED_EXPERIMENTS = %w[hashParams newCssCompiler].freeze
7
+
8
+ class << self
9
+ def call(package)
10
+ [no_symlinks(package.root), *no_experiments(package.manifest)].compact
11
+ end
12
+
13
+ private
14
+
15
+ def no_symlinks(path)
16
+ if Dir["#{path}/**/{*,.*}"].any? { |f| File.symlink?(f) }
17
+ return ValidationError.new(:symlink_in_zip)
18
+ end
19
+ nil
20
+ end
21
+
22
+ def no_experiments(manifest)
23
+ invalid_experiments = manifest.enabled_experiments - WHITELISTED_EXPERIMENTS
24
+ invalid_experiments.map do |experiment|
25
+ ValidationError.new(:invalid_experiment, experiment: experiment)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'mimemagic'
4
+
5
+ module ZendeskAppsSupport
6
+ module Validations
7
+ module Mime
8
+ UNSUPPORTED_MIME_TYPES = %w[
9
+ vnd.rar rar zip gzip pdf doc docx avi bin bz bz2 csh sh jar mp3 mpeg odt pptx ppt xls xlsx 7z
10
+ ].freeze
11
+
12
+ class << self
13
+ def call(package)
14
+ unsupported_files =
15
+ package.files.find_all { |app_file| block_listed?(app_file) }.map(&:relative_path)
16
+
17
+ [mime_type_warning(unsupported_files)] if unsupported_files.any?
18
+ end
19
+
20
+ private
21
+
22
+ def block_listed?(app_file)
23
+ mime_type = MimeMagic.by_magic(app_file.read)
24
+
25
+ content_subtype = mime_type.subtype if mime_type
26
+ extension_name = app_file.extension.delete('.')
27
+
28
+ ([content_subtype, extension_name] & UNSUPPORTED_MIME_TYPES).any?
29
+ end
30
+
31
+ def mime_type_warning(file_names)
32
+ ValidationError.new(
33
+ :unsupported_mime_type_detected,
34
+ file_names: file_names.join(', '),
35
+ count: file_names.count
36
+ )
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ipaddress_2'
4
+ require 'uri'
5
+
6
+ module ZendeskAppsSupport
7
+ module Validations
8
+ module Requests
9
+ class << self
10
+ def call(package)
11
+ files = package.js_files + package.html_files
12
+
13
+ files.each do |file|
14
+ file_content = file.read
15
+
16
+ http_protocol_urls = find_address_containing_http(file_content)
17
+ next unless http_protocol_urls.any?
18
+ package.warnings << insecure_http_requests_warning(
19
+ http_protocol_urls,
20
+ file.relative_path
21
+ )
22
+ end
23
+
24
+ package.warnings.flatten!
25
+ end
26
+
27
+ private
28
+
29
+ def insecure_http_requests_warning(http_protocol_urls, relative_path)
30
+ http_protocol_urls = http_protocol_urls.join(
31
+ I18n.t('txt.apps.admin.error.app_build.listing_comma')
32
+ )
33
+
34
+ I18n.t(
35
+ 'txt.apps.admin.warning.app_build.insecure_http_request',
36
+ uri: http_protocol_urls,
37
+ file: relative_path
38
+ )
39
+ end
40
+
41
+ def find_address_containing_http(file_content)
42
+ file_content.scan(URI.regexp(['http'])).map(&:compact).map(&:last)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ZendeskAppsSupport
4
+ module Validations
5
+ module Requirements
6
+ MAX_REQUIREMENTS = 5000
7
+ MAX_CUSTOM_OBJECTS_REQUIREMENTS = 50
8
+
9
+ class << self
10
+ def call(package)
11
+ if package.manifest.requirements_only? && !package.has_requirements?
12
+ return [ValidationError.new(:missing_requirements)]
13
+ elsif !supports_requirements(package) && package.has_requirements?
14
+ return [ValidationError.new(:requirements_not_supported)]
15
+ elsif !package.has_requirements?
16
+ return []
17
+ end
18
+
19
+ begin
20
+ requirements = package.requirements_json
21
+ rescue ZendeskAppsSupport::Manifest::OverrideError => e
22
+ return [ValidationError.new(:duplicate_requirements, duplicate_keys: e.key, count: 1)]
23
+ end
24
+
25
+ [].tap do |errors|
26
+ errors << invalid_requirements_types(requirements)
27
+ errors << excessive_requirements(requirements)
28
+ errors << excessive_custom_objects_requirements(requirements)
29
+ errors << invalid_channel_integrations(requirements)
30
+ errors << invalid_custom_fields(requirements)
31
+ errors << invalid_custom_objects(requirements)
32
+ errors << missing_required_fields(requirements)
33
+ errors.flatten!
34
+ errors.compact!
35
+ end
36
+ rescue JSON::ParserError => e
37
+ return [ValidationError.new(:requirements_not_json, errors: e)]
38
+ end
39
+
40
+ private
41
+
42
+ def supports_requirements(package)
43
+ !package.manifest.marketing_only? && package.manifest.products_ignore_locations != [Product::CHAT]
44
+ end
45
+
46
+ def missing_required_fields(requirements)
47
+ [].tap do |errors|
48
+ requirements.each do |requirement_type, requirement|
49
+ next if %w[channel_integrations custom_objects].include? requirement_type
50
+ requirement.each do |identifier, fields|
51
+ next if fields.nil? || fields.include?('title')
52
+ errors << ValidationError.new(:missing_required_fields,
53
+ field: 'title',
54
+ identifier: identifier)
55
+ end
56
+ end
57
+ end
58
+ end
59
+
60
+ def excessive_requirements(requirements)
61
+ count = requirements.values.map do |req|
62
+ req.is_a?(Hash) ? req.values : req
63
+ end.flatten.size
64
+ ValidationError.new(:excessive_requirements, max: MAX_REQUIREMENTS, count: count) if count > MAX_REQUIREMENTS
65
+ end
66
+
67
+ def excessive_custom_objects_requirements(requirements)
68
+ custom_objects = requirements[AppRequirement::CUSTOM_OBJECTS_KEY]
69
+ return unless custom_objects
70
+
71
+ count = custom_objects.values.flatten.size
72
+ if count > MAX_CUSTOM_OBJECTS_REQUIREMENTS
73
+ ValidationError.new(:excessive_custom_objects_requirements, max: MAX_CUSTOM_OBJECTS_REQUIREMENTS,
74
+ count: count)
75
+ end
76
+ end
77
+
78
+ def invalid_custom_fields(requirements)
79
+ user_fields = requirements['user_fields']
80
+ organization_fields = requirements['organization_fields']
81
+ return if user_fields.nil? && organization_fields.nil?
82
+ [].tap do |errors|
83
+ [user_fields, organization_fields].compact.each do |field_group|
84
+ field_group.each do |identifier, fields|
85
+ next if fields.include? 'key'
86
+ errors << ValidationError.new(:missing_required_fields,
87
+ field: 'key',
88
+ identifier: identifier)
89
+ end
90
+ end
91
+ end
92
+ end
93
+
94
+ def invalid_channel_integrations(requirements)
95
+ channel_integrations = requirements['channel_integrations']
96
+ return unless channel_integrations
97
+ [].tap do |errors|
98
+ if channel_integrations.size > 1
99
+ errors << ValidationError.new(:multiple_channel_integrations)
100
+ end
101
+ channel_integrations.each do |identifier, fields|
102
+ next if fields.include? 'manifest_url'
103
+ errors << ValidationError.new(:missing_required_fields,
104
+ field: 'manifest_url',
105
+ identifier: identifier)
106
+ end
107
+ end
108
+ end
109
+
110
+ def invalid_custom_objects(requirements)
111
+ custom_objects = requirements[AppRequirement::CUSTOM_OBJECTS_KEY]
112
+ return if custom_objects.nil?
113
+
114
+ [].tap do |errors|
115
+ unless custom_objects.key?(AppRequirement::CUSTOM_OBJECTS_TYPE_KEY)
116
+ errors << ValidationError.new(:missing_required_fields,
117
+ field: AppRequirement::CUSTOM_OBJECTS_TYPE_KEY,
118
+ identifier: AppRequirement::CUSTOM_OBJECTS_KEY)
119
+ end
120
+
121
+ valid_schema = {
122
+ AppRequirement::CUSTOM_OBJECTS_TYPE_KEY => %w[key schema],
123
+ AppRequirement::CUSTOM_OBJECTS_RELATIONSHIP_TYPE_KEY => %w[key source target]
124
+ }
125
+
126
+ valid_schema.keys.each do |requirement_type|
127
+ (custom_objects[requirement_type] || []).each do |requirement|
128
+ validate_custom_objects_keys(requirement.keys, valid_schema[requirement_type], requirement_type, errors)
129
+ end
130
+ end
131
+ end
132
+ end
133
+
134
+ def invalid_requirements_types(requirements)
135
+ invalid_types = requirements.keys - ZendeskAppsSupport::AppRequirement::TYPES
136
+ unless invalid_types.empty?
137
+ ValidationError.new(:invalid_requirements_types,
138
+ invalid_types: invalid_types.join(', '),
139
+ count: invalid_types.length)
140
+ end
141
+ end
142
+
143
+ def validate_custom_objects_keys(keys, expected_keys, identifier, errors = [])
144
+ missing_keys = expected_keys - keys
145
+ missing_keys.each do |key|
146
+ errors << ValidationError.new(:missing_required_fields,
147
+ field: key,
148
+ identifier: identifier)
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ZendeskAppsSupport
4
+ module Validations
5
+ module Secrets
6
+ SECRET_KEYWORDS = %w[
7
+ password secret secretToken secret_token auth_key
8
+ authKey auth_pass authPass auth_user AuthUser api_key
9
+ ].freeze
10
+
11
+ APPLICATION_SECRETS = {
12
+ # rubocop:disable Metrics/LineLength
13
+ 'Slack Token' => /(xox[p|b|o|a]-*.[a-z0-9])/,
14
+ 'RSA Private Key' => /-----BEGIN RSA PRIVATE KEY-----/,
15
+ 'SSH Private Key (OpenSSH)' => /-----BEGIN OPENSSH PRIVATE KEY-----/,
16
+ 'SSH Private Key (DSA)' => /-----BEGIN DSA PRIVATE KEY-----/,
17
+ 'SSH Private Key (EC)' => /-----BEGIN EC PRIVATE KEY-----/,
18
+ 'PGP Private Key Block' => /-----BEGIN PGP PRIVATE KEY BLOCK-----/,
19
+ 'Facebook OAuth Token' => /([f|F][a|A][c|C][e|E][b|B][o|O][o|O][k|K]( [|:\"=-]|[:\"=-|]).*.[0-9a-f]{24,36})/,
20
+ 'Twitter OAuth Token' => /([t|T][w|W][i|I][t|T][t|T][e|E][r|R]( [:\"=-]|[:\"=-]).*.[0-9a-zA-Z]{30,45})/,
21
+ 'Github Token' => /([g|G][i|I][t|T][h|H][u|U][b|B]( [:\"=-]|[:\"=-]).*.[0-9a-zA-Z]{30,45})/,
22
+ 'Google OAuth Token' => /([c|C][l|L][i|I][e|E][n|N][t|T][\-_][s|S][e|E][c|C][r|R][e|E][t|T]( [:\"=-]|[:\"=-]).*[a-zA-Z0-9\-_]{16,32})/,
23
+ 'AWS Access Key ID' => /(AKIA[0-9A-Z]{8,24})/,
24
+ 'AWS Secret Access Key' => /([a|A][w|W][s|S][_-][s|S][e|E][c|C][r|R][e|E][t|T][_-][a|A][c|C][c|C][e|E][s|S][s|S][_-][k|K][e|E][y|Y].*.[0-9a-zA-Z]{24,48})/,
25
+ 'Heroku API Key' => /([h|H][e|E][r|R][o|O][k|K][u|U].*[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{6,18})/,
26
+ 'Quickpay Secret' => /(quickpay_secret:.*.[0-9a-zA-Z]{24,72})/,
27
+ 'Doorman Secret' => /([d|D][o|O][o|O][r|R][m|M][a|A][n|N][-_][s|S][e|E][c|C][r|R][e|E][t|T].*.[0-9a-f]{16,132})/,
28
+ 'Shared Session Secret' => /(shared_session_secret.*.[0-9a-f]{4,132})/,
29
+ 'Permanent Cookie Secret' => /(permanent_cookie_secret.*.[0-9a-f]{120,156})/,
30
+ 'Scarlett AWS Secret Key' => /([sS][cC][aA][rR][lL][eE][tT][tT][_-][aA][wW][sS][_-][sS][eE][cC][rR][eE][tT][_-][kK][eE][yY].*.[0-9a-zA-Z+.]{35,45})/,
31
+ 'Braintree Key' => /(braintree_key.*.[0-9a-zA-Z]{16,36})/,
32
+ 'Ticket Validation Key' => /(ticket_validation_key.*.[0-9a-zA-Z]{15,25})/,
33
+ 'App Key' => /([aA][pP][pP][-_][kK][eE][yY]( [:\"=-]|[:\"=-]).*.[0-9a-zA-Z+_.-]{4,156})/,
34
+ 'App Secret' => /([aA][pP][pP][-_][sS][eE][cC][rR][eE][tT]( [:\"=-]|[:\"=-]).*.[0-9a-zA-Z+_.-]{4,156})/,
35
+ 'Consumer Key' => /([cC][oO][nN][sS][uU][mM][eE][rR][-_][kK][eE][yY]( [:\"=-]|[:\"=-]).*.[0-9a-zA-Z+_.-]{4,156})/,
36
+ 'Consumer Secret' => /([cC][oO][nN][sS][uU][mM][eE][rR][-_][sS][eE][cC][rR][eE][tT]( [:\"=-]|[:\"=-]).*.[0-9a-zA-Z+_.-]{4,156})/,
37
+ 'Generic Secret' => /(?m)^([sS][eE][cC][rR][eE][tT]( [:\"=-]|[:\"=-]).*.[0-9a-zA-Z+_.-]{4,156})/,
38
+ 'Master Key' => /([mM][aA][sS][tT][eE][rR][-_][kK][eE][yY]( [:\"=-]|[:\"=-]).*.[0-9a-zA-Z+_.-]{4,156})/,
39
+ 'Master Secret' => /([mM][aA][sS][tT][eE][rR][-_][sS][eE][cC][rR][eE][tT]( [:\"=-]|[:\"=-]).*.[0-9a-zA-Z+_.-]{4,156})/,
40
+ 'Token Key' => /([tT][oO][kK][eE][nN][-_][kK][eE][yY]( [:\"=-]|[:\"=-]).*.[0-9a-zA-Z+_.-]{4,156})/,
41
+ 'Token Secret' => /([tT][oO][kK][eE][nN][-_][sS][eE][cC][rR][eE][tT]( [:\"=-]|[:\"=-]).*.[0-9a-zA-Z+_.-]{4,156})/,
42
+ 'Zendesk Zopim Mobile SSO Key' => /(zendesk_zopim_mobile_sso_key.*.[0-9a-f]{58,68})/,
43
+ 'Help Center Private Key' => /([pP][rR][iI][vV][aA][tT][eE][-_][kK][eE][yY]( [:\"=-]|[:\"=-]).*.[0-9a-zA-Z+_.-]{4,156})/,
44
+ 'X-Outbound-Key' => /([xX][-][oO][uU][tT][bB][oO][uU][nN][dD][-][kK][eE][yY][:\" \t=-].*.[0-9a-z-]{32,36})/,
45
+ 'Attachment Token Key' => /(attachment_token_key.*.[0-9a-f]{24,72})/,
46
+ 'Password' => /([pP][aA][sS][sS][wW][oO][rR][dD].*.[0-9a-zA-Z+_.-]{4,156})/,
47
+ 'Token' => /([tT][oO][kK][eE][nN]( [:\"=-]|[:\"=-]).*.[0-9a-zA-Z+_.-]{4,156})/
48
+ # rubocop:enable Metrics/LineLength
49
+ }.freeze
50
+
51
+ class << self
52
+ def call(package)
53
+ compromised_files = package.text_files.map do |file|
54
+ contents = file.read
55
+
56
+ APPLICATION_SECRETS.each do |secret_type, regex_str|
57
+ next unless contents =~ Regexp.new(regex_str)
58
+ package.warnings << I18n.t('txt.apps.admin.warning.app_build.application_secret',
59
+ file: file.relative_path,
60
+ secret_type: secret_type)
61
+ end
62
+
63
+ match = Regexp.union(SECRET_KEYWORDS).match(contents)
64
+ "#{file.relative_path} ('#{match[0]}...')" if match
65
+ end.compact
66
+
67
+ return unless compromised_files.any?
68
+ package.warnings << I18n.t('txt.apps.admin.warning.app_build.generic_secrets',
69
+ files: compromised_files.join(
70
+ I18n.t('txt.apps.admin.error.app_build.listing_comma')
71
+ ),
72
+ count: compromised_files.count)
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ZendeskAppsSupport
4
+ module Validations
5
+ module SecureSettings
6
+ SECURABLE_KEYWORDS = %w[token key pwd password].freeze
7
+ SECURABLE_KEYWORDS_REGEXP = Regexp.new(SECURABLE_KEYWORDS.join('|'), Regexp::IGNORECASE)
8
+
9
+ class << self
10
+ def call(package)
11
+ manifest_params = package.manifest.parameters
12
+
13
+ insecure_params_found = manifest_params.any? { |param| insecure_param?(param) }
14
+
15
+ package.warnings << secure_settings_warning if insecure_params_found
16
+ end
17
+
18
+ private
19
+
20
+ def insecure_param?(parameter)
21
+ parameter.name =~ SECURABLE_KEYWORDS_REGEXP && type_password_or_text?(parameter.type) && !parameter.secure
22
+ end
23
+
24
+ def type_password_or_text?(parameter_type)
25
+ parameter_type == 'text' || parameter_type == 'password'
26
+ end
27
+
28
+ def secure_settings_warning
29
+ I18n.t(
30
+ 'txt.apps.admin.error.app_build.translation.insecure_token_parameter_in_manifest',
31
+ link: 'https://developer.zendesk.com/apps/docs/developer-guide/using_sdk#using-secure-settings'
32
+ )
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ZendeskAppsSupport
4
+ module Validations
5
+ module Source
6
+ class << self
7
+ def call(package)
8
+ if app_doesnt_require_source?(package.manifest) && contain_source_files?(package)
9
+ ValidationError.new(:no_source_required_apps)
10
+ end
11
+ end
12
+
13
+ private
14
+
15
+ def contain_source_files?(package)
16
+ package.js_files.any? || package.template_files.any? || !package.app_css.empty?
17
+ end
18
+
19
+ def app_doesnt_require_source?(manifest)
20
+ manifest.requirements_only? || manifest.marketing_only?
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'zendesk_apps_support/stylesheet_compiler'
4
+
5
+ module ZendeskAppsSupport
6
+ module Validations
7
+ module Stylesheets
8
+ class << self
9
+ def call(package)
10
+ css_error = validate_styles(package.app_css)
11
+ css_error ? [css_error] : []
12
+ end
13
+
14
+ private
15
+
16
+ def validate_styles(css)
17
+ compiler = ZendeskAppsSupport::StylesheetCompiler.new(css, nil, nil)
18
+ begin
19
+ compiler.compile
20
+ rescue SassC::SyntaxError, Sass::SyntaxError => e
21
+ return ValidationError.new(:stylesheet_error, sass_error: e.message)
22
+ end
23
+ nil
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end