zendesk_apps_support 4.29.4

Sign up to get free protection for your applications and to get access to all the features.
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