sealights-rspec-agent 2.0.4 → 2.0.5

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 (85) hide show
  1. checksums.yaml +4 -4
  2. data/agent/config.rb +6 -6
  3. data/agent/dependencies/faraday-0.17.0/LICENSE.md +20 -0
  4. data/agent/dependencies/faraday-0.17.0/README.md +384 -0
  5. data/agent/dependencies/faraday-0.17.0/lib/faraday.rb +248 -0
  6. data/agent/dependencies/faraday-0.17.0/lib/faraday/adapter.rb +55 -0
  7. data/agent/dependencies/faraday-0.17.0/lib/faraday/adapter/em_http.rb +243 -0
  8. data/agent/dependencies/faraday-0.17.0/lib/faraday/adapter/em_synchrony.rb +106 -0
  9. data/agent/dependencies/faraday-0.17.0/lib/faraday/adapter/em_synchrony/parallel_manager.rb +66 -0
  10. data/agent/dependencies/faraday-0.17.0/lib/faraday/adapter/excon.rb +82 -0
  11. data/agent/dependencies/faraday-0.17.0/lib/faraday/adapter/httpclient.rb +128 -0
  12. data/agent/dependencies/faraday-0.17.0/lib/faraday/adapter/net_http.rb +152 -0
  13. data/agent/dependencies/faraday-0.17.0/lib/faraday/adapter/net_http_persistent.rb +68 -0
  14. data/agent/dependencies/faraday-0.17.0/lib/faraday/adapter/patron.rb +95 -0
  15. data/agent/dependencies/faraday-0.17.0/lib/faraday/adapter/rack.rb +58 -0
  16. data/agent/dependencies/faraday-0.17.0/lib/faraday/adapter/sl_em_http_ssl_patch.rb +56 -0
  17. data/agent/dependencies/faraday-0.17.0/lib/faraday/adapter/test.rb +213 -0
  18. data/agent/dependencies/faraday-0.17.0/lib/faraday/adapter/typhoeus.rb +12 -0
  19. data/agent/dependencies/faraday-0.17.0/lib/faraday/autoload.rb +84 -0
  20. data/agent/dependencies/faraday-0.17.0/lib/faraday/connection.rb +484 -0
  21. data/agent/dependencies/faraday-0.17.0/lib/faraday/error.rb +66 -0
  22. data/agent/dependencies/faraday-0.17.0/lib/faraday/middleware.rb +37 -0
  23. data/agent/dependencies/faraday-0.17.0/lib/faraday/options.rb +373 -0
  24. data/agent/dependencies/faraday-0.17.0/lib/faraday/parameters.rb +198 -0
  25. data/agent/dependencies/faraday-0.17.0/lib/faraday/rack_builder.rb +237 -0
  26. data/agent/dependencies/faraday-0.17.0/lib/faraday/request.rb +114 -0
  27. data/agent/dependencies/faraday-0.17.0/lib/faraday/request/authorization.rb +41 -0
  28. data/agent/dependencies/faraday-0.17.0/lib/faraday/request/basic_authentication.rb +13 -0
  29. data/agent/dependencies/faraday-0.17.0/lib/faraday/request/instrumentation.rb +36 -0
  30. data/agent/dependencies/faraday-0.17.0/lib/faraday/request/multipart.rb +68 -0
  31. data/agent/dependencies/faraday-0.17.0/lib/faraday/request/retry.rb +212 -0
  32. data/agent/dependencies/faraday-0.17.0/lib/faraday/request/token_authentication.rb +15 -0
  33. data/agent/dependencies/faraday-0.17.0/lib/faraday/request/url_encoded.rb +36 -0
  34. data/agent/dependencies/faraday-0.17.0/lib/faraday/response.rb +97 -0
  35. data/agent/dependencies/faraday-0.17.0/lib/faraday/response/logger.rb +80 -0
  36. data/agent/dependencies/faraday-0.17.0/lib/faraday/response/raise_error.rb +21 -0
  37. data/agent/dependencies/faraday-0.17.0/lib/faraday/upload_io.rb +67 -0
  38. data/agent/dependencies/faraday-0.17.0/lib/faraday/utils.rb +326 -0
  39. data/agent/dependencies/jwt-2.2.1/AUTHORS +84 -0
  40. data/agent/dependencies/jwt-2.2.1/Appraisals +14 -0
  41. data/agent/dependencies/jwt-2.2.1/CHANGELOG.md +570 -0
  42. data/agent/dependencies/jwt-2.2.1/Gemfile +3 -0
  43. data/agent/dependencies/jwt-2.2.1/LICENSE +7 -0
  44. data/agent/dependencies/jwt-2.2.1/README.md +489 -0
  45. data/agent/dependencies/jwt-2.2.1/Rakefile +11 -0
  46. data/agent/dependencies/jwt-2.2.1/lib/jwt.rb +30 -0
  47. data/agent/dependencies/jwt-2.2.1/lib/jwt/algos/ecdsa.rb +35 -0
  48. data/agent/dependencies/jwt-2.2.1/lib/jwt/algos/eddsa.rb +23 -0
  49. data/agent/dependencies/jwt-2.2.1/lib/jwt/algos/hmac.rb +33 -0
  50. data/agent/dependencies/jwt-2.2.1/lib/jwt/algos/ps.rb +43 -0
  51. data/agent/dependencies/jwt-2.2.1/lib/jwt/algos/rsa.rb +19 -0
  52. data/agent/dependencies/jwt-2.2.1/lib/jwt/algos/unsupported.rb +16 -0
  53. data/agent/dependencies/jwt-2.2.1/lib/jwt/base64.rb +19 -0
  54. data/agent/dependencies/jwt-2.2.1/lib/jwt/claims_validator.rb +33 -0
  55. data/agent/dependencies/jwt-2.2.1/lib/jwt/decode.rb +100 -0
  56. data/agent/dependencies/jwt-2.2.1/lib/jwt/default_options.rb +15 -0
  57. data/agent/dependencies/jwt-2.2.1/lib/jwt/encode.rb +68 -0
  58. data/agent/dependencies/jwt-2.2.1/lib/jwt/error.rb +20 -0
  59. data/agent/dependencies/jwt-2.2.1/lib/jwt/json.rb +18 -0
  60. data/agent/dependencies/jwt-2.2.1/lib/jwt/jwk.rb +31 -0
  61. data/agent/dependencies/jwt-2.2.1/lib/jwt/jwk/key_finder.rb +57 -0
  62. data/agent/dependencies/jwt-2.2.1/lib/jwt/jwk/rsa.rb +47 -0
  63. data/agent/dependencies/jwt-2.2.1/lib/jwt/security_utils.rb +57 -0
  64. data/agent/dependencies/jwt-2.2.1/lib/jwt/signature.rb +52 -0
  65. data/agent/dependencies/jwt-2.2.1/lib/jwt/verify.rb +98 -0
  66. data/agent/dependencies/jwt-2.2.1/lib/jwt/version.rb +24 -0
  67. data/agent/dependencies/jwt-2.2.1/ruby-jwt.gemspec +34 -0
  68. data/agent/dependencies/multipart-post-2.1.1/Gemfile +6 -0
  69. data/agent/dependencies/multipart-post-2.1.1/History.txt +64 -0
  70. data/agent/dependencies/multipart-post-2.1.1/LICENSE +21 -0
  71. data/agent/dependencies/multipart-post-2.1.1/Manifest.txt +9 -0
  72. data/agent/dependencies/multipart-post-2.1.1/README.md +127 -0
  73. data/agent/dependencies/multipart-post-2.1.1/Rakefile +6 -0
  74. data/agent/dependencies/multipart-post-2.1.1/lib/composite_io.rb +108 -0
  75. data/agent/dependencies/multipart-post-2.1.1/lib/multipart_post.rb +9 -0
  76. data/agent/dependencies/multipart-post-2.1.1/lib/multipartable.rb +48 -0
  77. data/agent/dependencies/multipart-post-2.1.1/lib/net/http/post/multipart.rb +28 -0
  78. data/agent/dependencies/multipart-post-2.1.1/lib/parts.rb +126 -0
  79. data/agent/dependencies/multipart-post-2.1.1/multipart-post.gemspec +23 -0
  80. data/agent/http_client.rb +46 -0
  81. data/agent/listener.rb +1 -1
  82. data/agent/sealights-rspec-agent.rb +2 -2
  83. data/agent/tia.rb +5 -1
  84. metadata +80 -3
  85. data/agent/rest-client-wrapper.rb +0 -27
@@ -0,0 +1,11 @@
1
+ require 'bundler/gem_tasks'
2
+
3
+ begin
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:test)
7
+
8
+ task default: :test
9
+ rescue LoadError
10
+ puts 'RSpec rake tasks not available. Please run "bundle install" to install missing dependencies.'
11
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'jwt/base64'
4
+ require_relative 'jwt/json'
5
+ require_relative 'jwt/decode'
6
+ require_relative 'jwt/default_options'
7
+ require_relative 'jwt/encode'
8
+ require_relative 'jwt/error'
9
+ require_relative 'jwt/jwk'
10
+
11
+ # JSON Web Token implementation
12
+ #
13
+ # Should be up to date with the latest spec:
14
+ # https://tools.ietf.org/html/rfc7519
15
+ module SL_JWT
16
+ include SL_JWT::DefaultOptions
17
+
18
+ module_function
19
+
20
+ def encode(payload, key, algorithm = 'HS256', header_fields = {})
21
+ Encode.new(payload: payload,
22
+ key: key,
23
+ algorithm: algorithm,
24
+ headers: header_fields).segments
25
+ end
26
+
27
+ def decode(jwt, key = nil, verify = true, options = {}, &keyfinder)
28
+ Decode.new(jwt, key, verify, DEFAULT_OPTIONS.merge(options), &keyfinder).decode_segments
29
+ end
30
+ end
@@ -0,0 +1,35 @@
1
+ module SL_JWT
2
+ module Algos
3
+ module Ecdsa
4
+ module_function
5
+
6
+ SUPPORTED = %w[ES256 ES384 ES512].freeze
7
+ NAMED_CURVES = {
8
+ 'prime256v1' => 'ES256',
9
+ 'secp384r1' => 'ES384',
10
+ 'secp521r1' => 'ES512'
11
+ }.freeze
12
+
13
+ def sign(to_sign)
14
+ algorithm, msg, key = to_sign.values
15
+ key_algorithm = NAMED_CURVES[key.group.curve_name]
16
+ if algorithm != key_algorithm
17
+ raise IncorrectAlgorithm, "payload algorithm is #{algorithm} but #{key_algorithm} signing key was provided"
18
+ end
19
+
20
+ digest = OpenSSL::Digest.new(algorithm.sub('ES', 'sha'))
21
+ SecurityUtils.asn1_to_raw(key.dsa_sign_asn1(digest.digest(msg)), key)
22
+ end
23
+
24
+ def verify(to_verify)
25
+ algorithm, public_key, signing_input, signature = to_verify.values
26
+ key_algorithm = NAMED_CURVES[public_key.group.curve_name]
27
+ if algorithm != key_algorithm
28
+ raise IncorrectAlgorithm, "payload algorithm is #{algorithm} but #{key_algorithm} verification key was provided"
29
+ end
30
+ digest = OpenSSL::Digest.new(algorithm.sub('ES', 'sha'))
31
+ public_key.dsa_verify_asn1(digest.digest(signing_input), SecurityUtils.raw_to_asn1(signature, public_key))
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,23 @@
1
+ module SL_JWT
2
+ module Algos
3
+ module Eddsa
4
+ module_function
5
+
6
+ SUPPORTED = %w[ED25519].freeze
7
+
8
+ def sign(to_sign)
9
+ algorithm, msg, key = to_sign.values
10
+ raise EncodeError, "Key given is a #{key.class} but has to be an RbNaCl::Signatures::Ed25519::SigningKey" if key.class != RbNaCl::Signatures::Ed25519::SigningKey
11
+ raise IncorrectAlgorithm, "payload algorithm is #{algorithm} but #{key.primitive} signing key was provided" if algorithm.downcase.to_sym != key.primitive
12
+ key.sign(msg)
13
+ end
14
+
15
+ def verify(to_verify)
16
+ algorithm, public_key, signing_input, signature = to_verify.values
17
+ raise IncorrectAlgorithm, "payload algorithm is #{algorithm} but #{public_key.primitive} verification key was provided" if algorithm.downcase.to_sym != public_key.primitive
18
+ raise DecodeError, "key given is a #{public_key.class} but has to be a RbNaCl::Signatures::Ed25519::VerifyKey" if public_key.class != RbNaCl::Signatures::Ed25519::VerifyKey
19
+ public_key.verify(signature, signing_input)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,33 @@
1
+ module SL_JWT
2
+ module Algos
3
+ module Hmac
4
+ module_function
5
+
6
+ SUPPORTED = %w[HS256 HS512256 HS384 HS512].freeze
7
+
8
+ def sign(to_sign)
9
+ algorithm, msg, key = to_sign.values
10
+ authenticator, padded_key = SecurityUtils.rbnacl_fixup(algorithm, key)
11
+ if authenticator && padded_key
12
+ authenticator.auth(padded_key, msg.encode('binary'))
13
+ else
14
+ OpenSSL::HMAC.digest(OpenSSL::Digest.new(algorithm.sub('HS', 'sha')), key, msg)
15
+ end
16
+ end
17
+
18
+ def verify(to_verify)
19
+ algorithm, public_key, signing_input, signature = to_verify.values
20
+ authenticator, padded_key = SecurityUtils.rbnacl_fixup(algorithm, public_key)
21
+ if authenticator && padded_key
22
+ begin
23
+ authenticator.verify(padded_key, signature.encode('binary'), signing_input.encode('binary'))
24
+ rescue RbNaCl::BadAuthenticatorError
25
+ false
26
+ end
27
+ else
28
+ SecurityUtils.secure_compare(signature, sign(SL_JWT::Signature::ToSign.new(algorithm, signing_input, public_key)))
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,43 @@
1
+ module SL_JWT
2
+ module Algos
3
+ module Ps
4
+ # RSASSA-PSS signing algorithms
5
+
6
+ module_function
7
+
8
+ SUPPORTED = %w[PS256 PS384 PS512].freeze
9
+
10
+ def sign(to_sign)
11
+ require_openssl!
12
+
13
+ algorithm, msg, key = to_sign.values
14
+
15
+ key_class = key.class
16
+
17
+ raise EncodeError, "The given key is a #{key_class}. It has to be an OpenSSL::PKey::RSA instance." if key_class == String
18
+
19
+ translated_algorithm = algorithm.sub('PS', 'sha')
20
+
21
+ key.sign_pss(translated_algorithm, msg, salt_length: :digest, mgf1_hash: translated_algorithm)
22
+ end
23
+
24
+ def verify(to_verify)
25
+ require_openssl!
26
+
27
+ SecurityUtils.verify_ps(to_verify.algorithm, to_verify.public_key, to_verify.signing_input, to_verify.signature)
28
+ end
29
+
30
+ def require_openssl!
31
+ if Object.const_defined?('OpenSSL')
32
+ major, minor = OpenSSL::VERSION.split('.').first(2)
33
+
34
+ unless major.to_i >= 2 && minor.to_i >= 1
35
+ raise SL_JWT::RequiredDependencyError, "You currently have OpenSSL #{OpenSSL::VERSION}. PS support requires >= 2.1"
36
+ end
37
+ else
38
+ raise SL_JWT::RequiredDependencyError, 'PS signing requires OpenSSL +2.1'
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,19 @@
1
+ module SL_JWT
2
+ module Algos
3
+ module Rsa
4
+ module_function
5
+
6
+ SUPPORTED = %w[RS256 RS384 RS512].freeze
7
+
8
+ def sign(to_sign)
9
+ algorithm, msg, key = to_sign.values
10
+ raise EncodeError, "The given key is a #{key.class}. It has to be an OpenSSL::PKey::RSA instance." if key.class == String
11
+ key.sign(OpenSSL::Digest.new(algorithm.sub('RS', 'sha')), msg)
12
+ end
13
+
14
+ def verify(to_verify)
15
+ SecurityUtils.verify_rsa(to_verify.algorithm, to_verify.public_key, to_verify.signing_input, to_verify.signature)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,16 @@
1
+ module SL_JWT
2
+ module Algos
3
+ module Unsupported
4
+ module_function
5
+
6
+ SUPPORTED = Object.new.tap { |object| object.define_singleton_method(:include?) { |*| true } }
7
+ def verify(*)
8
+ raise SL_JWT::VerificationError, 'Algorithm not supported'
9
+ end
10
+
11
+ def sign(*)
12
+ raise NotImplementedError, 'Unsupported signing method'
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+
5
+ module SL_JWT
6
+ # Base64 helpers
7
+ class Base64
8
+ class << self
9
+ def url_encode(str)
10
+ ::Base64.encode64(str).tr('+/', '-_').gsub(/[\n=]/, '')
11
+ end
12
+
13
+ def url_decode(str)
14
+ str += '=' * (4 - str.length.modulo(4))
15
+ ::Base64.decode64(str.tr('-_', '+/'))
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,33 @@
1
+ require_relative './error'
2
+
3
+ module SL_JWT
4
+ class ClaimsValidator
5
+ INTEGER_CLAIMS = %i[
6
+ exp
7
+ iat
8
+ nbf
9
+ ].freeze
10
+
11
+ def initialize(payload)
12
+ @payload = payload.each_with_object({}) { |(k, v), h| h[k.to_sym] = v }
13
+ end
14
+
15
+ def validate!
16
+ validate_int_claims
17
+
18
+ true
19
+ end
20
+
21
+ private
22
+
23
+ def validate_int_claims
24
+ INTEGER_CLAIMS.each do |claim|
25
+ validate_is_int(claim) if @payload.key?(claim)
26
+ end
27
+ end
28
+
29
+ def validate_is_int(claim)
30
+ raise InvalidPayload, "#{claim} claim must be an Integer but it is a #{@payload[claim].class}" unless @payload[claim].is_a?(Integer)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ require_relative './signature'
6
+ require_relative './verify'
7
+ # JWT::Decode module
8
+ module SL_JWT
9
+ # Decoding logic for JWT
10
+ class Decode
11
+ def initialize(jwt, key, verify, options, &keyfinder)
12
+ raise(SL_JWT::DecodeError, 'Nil JSON web token') unless jwt
13
+ @jwt = jwt
14
+ @key = key
15
+ @options = options
16
+ @segments = jwt.split('.')
17
+ @verify = verify
18
+ @signature = ''
19
+ @keyfinder = keyfinder
20
+ end
21
+
22
+ def decode_segments
23
+ validate_segment_count!
24
+ if @verify
25
+ decode_crypto
26
+ verify_signature
27
+ verify_claims
28
+ end
29
+ raise(SL_JWT::DecodeError, 'Not enough or too many segments') unless header && payload
30
+ [payload, header]
31
+ end
32
+
33
+ private
34
+
35
+ def verify_signature
36
+ @key = find_key(&@keyfinder) if @keyfinder
37
+ @key = ::SL_JWT::JWK::KeyFinder.new(jwks: @options[:jwks]).key_for(header['kid']) if @options[:jwks]
38
+
39
+ raise(SL_JWT::IncorrectAlgorithm, 'An algorithm must be specified') if allowed_algorithms.empty?
40
+ raise(SL_JWT::IncorrectAlgorithm, 'Expected a different algorithm') unless options_includes_algo_in_header?
41
+
42
+ Signature.verify(header['alg'], @key, signing_input, @signature)
43
+ end
44
+
45
+ def options_includes_algo_in_header?
46
+ allowed_algorithms.include? header['alg']
47
+ end
48
+
49
+ def allowed_algorithms
50
+ if @options.key?(:algorithm)
51
+ [@options[:algorithm]]
52
+ else
53
+ @options[:algorithms] || []
54
+ end
55
+ end
56
+
57
+ def find_key(&keyfinder)
58
+ key = (keyfinder.arity == 2 ? yield(header, payload) : yield(header))
59
+ raise SL_JWT::DecodeError, 'No verification key available' unless key
60
+ key
61
+ end
62
+
63
+ def verify_claims
64
+ Verify.verify_claims(payload, @options)
65
+ end
66
+
67
+ def validate_segment_count!
68
+ return if segment_length == 3
69
+ return if !@verify && segment_length == 2 # If no verifying required, the signature is not needed
70
+
71
+ raise(SL_JWT::DecodeError, 'Not enough or too many segments')
72
+ end
73
+
74
+ def segment_length
75
+ @segments.count
76
+ end
77
+
78
+ def decode_crypto
79
+ @signature = SL_JWT::Base64.url_decode(@segments[2])
80
+ end
81
+
82
+ def header
83
+ @header ||= parse_and_decode @segments[0]
84
+ end
85
+
86
+ def payload
87
+ @payload ||= parse_and_decode @segments[1]
88
+ end
89
+
90
+ def signing_input
91
+ @segments.first(2).join('.')
92
+ end
93
+
94
+ def parse_and_decode(segment)
95
+ SL_JWT::JSON.parse(SL_JWT::Base64.url_decode(segment))
96
+ rescue ::JSON::ParserError
97
+ raise SL_JWT::DecodeError, 'Invalid segment encoding'
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,15 @@
1
+ module SL_JWT
2
+ module DefaultOptions
3
+ DEFAULT_OPTIONS = {
4
+ verify_expiration: true,
5
+ verify_not_before: true,
6
+ verify_iss: false,
7
+ verify_iat: false,
8
+ verify_jti: false,
9
+ verify_aud: false,
10
+ verify_sub: false,
11
+ leeway: 0,
12
+ algorithms: ['HS256']
13
+ }.freeze
14
+ end
15
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './claims_validator'
4
+
5
+ # JWT::Encode module
6
+ module SL_JWT
7
+ # Encoding logic for JWT
8
+ class Encode
9
+ ALG_NONE = 'none'.freeze
10
+ ALG_KEY = 'alg'.freeze
11
+
12
+ def initialize(options)
13
+ @payload = options[:payload]
14
+ @key = options[:key]
15
+ @algorithm = options[:algorithm]
16
+ @headers = options[:headers].each_with_object({}) { |(key, value), headers| headers[key.to_s] = value }
17
+ end
18
+
19
+ def segments
20
+ @segments ||= combine(encoded_header_and_payload, encoded_signature)
21
+ end
22
+
23
+ private
24
+
25
+ def encoded_header
26
+ @encoded_header ||= encode_header
27
+ end
28
+
29
+ def encoded_payload
30
+ @encoded_payload ||= encode_payload
31
+ end
32
+
33
+ def encoded_signature
34
+ @encoded_signature ||= encode_signature
35
+ end
36
+
37
+ def encoded_header_and_payload
38
+ @encoded_header_and_payload ||= combine(encoded_header, encoded_payload)
39
+ end
40
+
41
+ def encode_header
42
+ @headers[ALG_KEY] = @algorithm
43
+ encode(@headers)
44
+ end
45
+
46
+ def encode_payload
47
+ if @payload && @payload.is_a?(Hash)
48
+ ClaimsValidator.new(@payload).validate!
49
+ end
50
+
51
+ encode(@payload)
52
+ end
53
+
54
+ def encode_signature
55
+ return '' if @algorithm == ALG_NONE
56
+
57
+ SL_JWT::Base64.url_encode(SL_JWT::Signature.sign(@algorithm, encoded_header_and_payload, @key))
58
+ end
59
+
60
+ def encode(data)
61
+ SL_JWT::Base64.url_encode(SL_JWT::JSON.generate(data))
62
+ end
63
+
64
+ def combine(*parts)
65
+ parts.join('.')
66
+ end
67
+ end
68
+ end