securenative 0.1.29 → 0.1.30
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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +49 -0
- data/.github/workflows/publish.yml +60 -0
- data/.github/workflows/test.yml +48 -0
- data/.gitignore +40 -0
- data/.rakeTasks +7 -0
- data/.rspec +3 -0
- data/Gemfile +11 -0
- data/Gemfile.lock +270 -0
- data/LICENSE +21 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/securenative/api_manager.rb +34 -0
- data/lib/securenative/client.rb +75 -0
- data/lib/securenative/client_token.rb +14 -0
- data/lib/securenative/config/configuration_builder.rb +29 -0
- data/lib/securenative/config/configuration_manager.rb +57 -0
- data/lib/securenative/context.rb +65 -0
- data/lib/securenative/device.rb +12 -0
- data/lib/securenative/enums/api_route.rb +10 -0
- data/lib/securenative/enums/risk_level.rb +11 -0
- data/lib/securenative/errors/config_error.rb +4 -0
- data/lib/securenative/errors/http_error.rb +4 -0
- data/lib/securenative/errors/invalid_options_error.rb +4 -0
- data/lib/securenative/errors/invalid_uri_error.rb +6 -0
- data/lib/securenative/errors/parse_error.rb +4 -0
- data/lib/securenative/errors/sdk_Illegal_state_error.rb +4 -0
- data/lib/securenative/errors/sdk_error.rb +4 -0
- data/lib/securenative/event_manager.rb +156 -0
- data/lib/securenative/event_options.rb +35 -0
- data/lib/securenative/event_types.rb +25 -0
- data/lib/securenative/failover_strategy.rb +8 -0
- data/lib/securenative/frameworks/hanami.rb +46 -0
- data/lib/securenative/frameworks/rails.rb +48 -0
- data/lib/securenative/frameworks/sinatra.rb +46 -0
- data/lib/securenative/http_client.rb +47 -0
- data/lib/securenative/http_response.rb +14 -0
- data/lib/securenative/options.rb +23 -0
- data/lib/securenative/request_context.rb +20 -0
- data/lib/securenative/request_options.rb +14 -0
- data/lib/securenative/sdk_event.rb +44 -0
- data/lib/securenative/user_traits.rb +15 -0
- data/lib/securenative/utils/date_utils.rb +13 -0
- data/lib/securenative/utils/encryption_utils.rb +48 -0
- data/lib/securenative/utils/ip_utils.rb +25 -0
- data/lib/securenative/utils/log.rb +46 -0
- data/lib/securenative/utils/request_utils.rb +84 -0
- data/lib/securenative/utils/signature_utils.rb +18 -0
- data/lib/securenative/utils/utils.rb +13 -0
- data/lib/securenative/utils/version_utils.rb +15 -0
- data/lib/securenative/verify_result.rb +18 -0
- data/lib/securenative/version.rb +5 -0
- data/securenative.gemspec +33 -0
- metadata +55 -2
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SecureNative
|
4
|
+
class EventOptions
|
5
|
+
attr_reader :event, :user_id, :user_traits, :context, :properties, :timestamp
|
6
|
+
attr_writer :event, :user_id, :user_traits, :context, :properties, :timestamp
|
7
|
+
|
8
|
+
MAX_PROPERTIES_SIZE = 10
|
9
|
+
|
10
|
+
def initialize(event: nil, user_id: nil, user_traits: nil, user_name: nil, email: nil, phone: nil, created_at: nil, context: nil, properties: nil, timestamp: nil)
|
11
|
+
if !properties.nil? && properties.length > MAX_PROPERTIES_SIZE
|
12
|
+
raise SecureNativeInvalidOptionsError, "You can have only up to #{MAX_PROPERTIES_SIZE} custom properties"
|
13
|
+
end
|
14
|
+
|
15
|
+
if user_traits.nil?
|
16
|
+
if user_name && email && phone && created_at
|
17
|
+
user_traits = SecureNative::UserTraits(user_name, email, phone, created_at)
|
18
|
+
elsif user_name && email && phone
|
19
|
+
user_traits = SecureNative::UserTraits(user_name, email, phone)
|
20
|
+
elsif user_name && email
|
21
|
+
user_traits = SecureNative::UserTraits(user_name, email)
|
22
|
+
else
|
23
|
+
user_traits = UserTraits.new
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
@event = event
|
28
|
+
@user_id = user_id
|
29
|
+
@user_traits = user_traits
|
30
|
+
@context = context
|
31
|
+
@properties = properties
|
32
|
+
@timestamp = timestamp
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SecureNative
|
4
|
+
module EventTypes
|
5
|
+
LOG_IN = 'sn.user.login'
|
6
|
+
LOG_IN_CHALLENGE = 'sn.user.login.challenge'
|
7
|
+
LOG_IN_FAILURE = 'sn.user.login.failure'
|
8
|
+
LOG_OUT = 'sn.user.logout'
|
9
|
+
SIGN_UP = 'sn.user.signup'
|
10
|
+
AUTH_CHALLENGE = 'sn.user.auth.challenge'
|
11
|
+
AUTH_CHALLENGE_SUCCESS = 'sn.user.auth.challenge.success'
|
12
|
+
AUTH_CHALLENGE_FAILURE = 'sn.user.auth.challenge.failure'
|
13
|
+
TWO_FACTOR_DISABLE = 'sn.user.2fa.disable'
|
14
|
+
EMAIL_UPDATE = 'sn.user.email.update'
|
15
|
+
PASSWORD_REST = 'sn.user.password.reset'
|
16
|
+
PASSWORD_REST_SUCCESS = 'sn.user.password.reset.success'
|
17
|
+
PASSWORD_UPDATE = 'sn.user.password.update'
|
18
|
+
PASSWORD_REST_FAILURE = 'sn.user.password.reset.failure'
|
19
|
+
USER_INVITE = 'sn.user.invite'
|
20
|
+
ROLE_UPDATE = 'sn.user.role.update'
|
21
|
+
PROFILE_UPDATE = 'sn.user.profile.update'
|
22
|
+
PAGE_VIEW = 'sn.user.page.view'
|
23
|
+
VERIFY = 'sn.verify'
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SecureNative
|
4
|
+
module Frameworks
|
5
|
+
class Hanami
|
6
|
+
SECURENATIVE_COOKIE = '_sn'
|
7
|
+
|
8
|
+
def self.get_client_token(request)
|
9
|
+
begin
|
10
|
+
request.env[SECURENATIVE_COOKIE]
|
11
|
+
rescue StandardError
|
12
|
+
begin
|
13
|
+
request.cookies[SECURENATIVE_COOKIE]
|
14
|
+
rescue StandardError
|
15
|
+
nil
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.get_url(request)
|
21
|
+
begin
|
22
|
+
request.env['REQUEST_PATH']
|
23
|
+
rescue StandardError
|
24
|
+
nil
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.get_method(request)
|
29
|
+
begin
|
30
|
+
request.request_method
|
31
|
+
rescue StandardError
|
32
|
+
nil
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.get_headers(request)
|
37
|
+
begin
|
38
|
+
# Note: At the moment we're filtering out everything but user-agent since ruby's payload is way too big
|
39
|
+
{ 'user-agent' => request.env['HTTP_USER_AGENT'] }
|
40
|
+
rescue StandardError
|
41
|
+
nil
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SecureNative
|
4
|
+
module Frameworks
|
5
|
+
class Rails
|
6
|
+
SECURENATIVE_COOKIE = '_sn'
|
7
|
+
|
8
|
+
def self.get_client_token(request)
|
9
|
+
begin
|
10
|
+
request.cookies[SECURENATIVE_COOKIE]
|
11
|
+
rescue StandardError
|
12
|
+
nil
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.get_url(request)
|
17
|
+
begin
|
18
|
+
# Rails >= 3.x
|
19
|
+
request.fullpath
|
20
|
+
rescue StandardError
|
21
|
+
begin
|
22
|
+
# Rails < 3.x & Sinatra
|
23
|
+
request.url if url.nil?
|
24
|
+
rescue StandardError
|
25
|
+
nil
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.get_method(request)
|
31
|
+
begin
|
32
|
+
request.method
|
33
|
+
rescue StandardError
|
34
|
+
nil
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.get_headers(request)
|
39
|
+
begin
|
40
|
+
# Note: At the moment we're filtering out everything but user-agent since ruby's payload is way too big
|
41
|
+
{'user-agent' => request.env['HTTP_USER_AGENT']}
|
42
|
+
rescue StandardError
|
43
|
+
nil
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SecureNative
|
4
|
+
module Frameworks
|
5
|
+
class Sinatra
|
6
|
+
SECURENATIVE_COOKIE = '_sn'
|
7
|
+
|
8
|
+
def self.get_client_token(request)
|
9
|
+
begin
|
10
|
+
request.env[SECURENATIVE_COOKIE]
|
11
|
+
rescue StandardError
|
12
|
+
begin
|
13
|
+
request.cookies[SECURENATIVE_COOKIE]
|
14
|
+
rescue StandardError
|
15
|
+
nil
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.get_url(request)
|
21
|
+
begin
|
22
|
+
request.env['REQUEST_URI']
|
23
|
+
rescue StandardError
|
24
|
+
nil
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.get_method(request)
|
29
|
+
begin
|
30
|
+
request.env['REQUEST_METHOD']
|
31
|
+
rescue StandardError
|
32
|
+
nil
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.get_headers(request)
|
37
|
+
begin
|
38
|
+
# Note: At the moment we're filtering out everything but user-agent since ruby's payload is way too big
|
39
|
+
{'user-agent' => request.env['HTTP_USER_AGENT']}
|
40
|
+
rescue StandardError
|
41
|
+
nil
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SecureNative
|
4
|
+
class HttpClient
|
5
|
+
AUTHORIZATION_HEADER = 'Authorization'
|
6
|
+
VERSION_HEADER = 'SN-Version'
|
7
|
+
USER_AGENT_HEADER = 'User-Agent'
|
8
|
+
USER_AGENT_HEADER_VALUE = 'SecureNative-ruby'
|
9
|
+
CONTENT_TYPE_HEADER = 'Content-Type'
|
10
|
+
CONTENT_TYPE_HEADER_VALUE = 'application/json'
|
11
|
+
|
12
|
+
def initialize(securenative_options)
|
13
|
+
@options = securenative_options
|
14
|
+
end
|
15
|
+
|
16
|
+
def _headers
|
17
|
+
{
|
18
|
+
CONTENT_TYPE_HEADER => CONTENT_TYPE_HEADER_VALUE,
|
19
|
+
USER_AGENT_HEADER => USER_AGENT_HEADER_VALUE,
|
20
|
+
VERSION_HEADER => SecureNative::Utils::VersionUtils.version,
|
21
|
+
AUTHORIZATION_HEADER => @options.api_key
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
def post(path, body)
|
26
|
+
uri = URI.parse("#{@options.api_url}/#{path}")
|
27
|
+
headers = _headers
|
28
|
+
|
29
|
+
client = Net::HTTP.new(uri.host, uri.port)
|
30
|
+
client.read_timeout = @options.timeout
|
31
|
+
client.use_ssl = true
|
32
|
+
client.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
33
|
+
|
34
|
+
request = Net::HTTP::Post.new(uri.request_uri, headers)
|
35
|
+
request.body = body
|
36
|
+
|
37
|
+
res = nil
|
38
|
+
begin
|
39
|
+
res = client.request(request)
|
40
|
+
rescue StandardError => e
|
41
|
+
SecureNative::Log.error("Failed to send request; #{e}")
|
42
|
+
return res
|
43
|
+
end
|
44
|
+
res
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SecureNative
|
4
|
+
class HttpResponse
|
5
|
+
attr_reader :ok, :status_code, :body
|
6
|
+
attr_writer :ok, :status_code, :body
|
7
|
+
|
8
|
+
def initialize(ok, status_code, body)
|
9
|
+
@ok = ok
|
10
|
+
@status_code = status_code
|
11
|
+
@body = body
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SecureNative
|
4
|
+
class Options
|
5
|
+
attr_reader :api_key, :api_url, :interval, :max_events, :timeout, :auto_send, :disable, :log_level, :fail_over_strategy, :proxy_headers
|
6
|
+
attr_writer :api_key, :api_url, :interval, :max_events, :timeout, :auto_send, :disable, :log_level, :fail_over_strategy, :proxy_headers
|
7
|
+
|
8
|
+
def initialize(api_key: nil, api_url: "https://api.securenative.com/collector/api/v1", interval: 1000,
|
9
|
+
max_events: 1000, timeout: 1500, auto_send: true, disable: false, log_level: "FATAL",
|
10
|
+
fail_over_strategy: FailOverStrategy::FAIL_OPEN, proxy_headers: nil)
|
11
|
+
@api_key = api_key
|
12
|
+
@api_url = api_url
|
13
|
+
@interval = interval
|
14
|
+
@max_events = max_events
|
15
|
+
@timeout = timeout
|
16
|
+
@auto_send = auto_send
|
17
|
+
@disable = disable
|
18
|
+
@log_level = log_level
|
19
|
+
@fail_over_strategy = fail_over_strategy
|
20
|
+
@proxy_headers = proxy_headers
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SecureNative
|
4
|
+
class RequestContext
|
5
|
+
attr_reader :cid, :vid, :fp, :ip, :remote_ip, :headers, :url, :http_method
|
6
|
+
attr_writer :cid, :vid, :fp, :ip, :remote_ip, :headers, :url, :http_method
|
7
|
+
|
8
|
+
def initialize(cid: nil, vid: nil, fp: nil, ip: nil, remote_ip: nil, headers: nil, url: nil, http_method: nil)
|
9
|
+
@cid = cid
|
10
|
+
@vid = vid
|
11
|
+
@fp = fp
|
12
|
+
@ip = ip
|
13
|
+
@remote_ip = remote_ip
|
14
|
+
@headers = headers
|
15
|
+
@url = url
|
16
|
+
@method = http_method
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SecureNative
|
4
|
+
class RequestOptions
|
5
|
+
attr_reader :url, :body, :retry_sending
|
6
|
+
attr_writer :url, :body, :retry_sending
|
7
|
+
|
8
|
+
def initialize(url, body, retry_sending)
|
9
|
+
@url = url
|
10
|
+
@body = body
|
11
|
+
@retry_sending = retry_sending
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SecureNative
|
4
|
+
class SDKEvent
|
5
|
+
attr_reader :context, :rid, :event_type, :user_id, :user_traits, :request, :timestamp, :properties
|
6
|
+
attr_writer :context, :rid, :event_type, :user_id, :user_traits, :request, :timestamp, :properties
|
7
|
+
|
8
|
+
def initialize(event_options, securenative_options)
|
9
|
+
if event_options.user_id.nil? || event_options.user_id.length <= 0 || event_options.user_id == ''
|
10
|
+
raise SecureNativeInvalidOptionsError.new, 'Invalid event structure; User Id is missing'
|
11
|
+
end
|
12
|
+
|
13
|
+
if event_options.event.nil? || event_options.event.length <= 0 || event_options.event == ''
|
14
|
+
raise SecureNativeInvalidOptionsError.new, 'Invalid event structure; Event Type is missing'
|
15
|
+
end
|
16
|
+
|
17
|
+
@context = if !event_options.context.nil?
|
18
|
+
event_options.context
|
19
|
+
else
|
20
|
+
Context.default_context_builder
|
21
|
+
end
|
22
|
+
|
23
|
+
client_token = SecureNative::Utils::EncryptionUtils.decrypt(@context.client_token, securenative_options.api_key)
|
24
|
+
|
25
|
+
@rid = SecureRandom.uuid.to_str
|
26
|
+
@event_type = event_options.event
|
27
|
+
@user_id = event_options.user_id
|
28
|
+
@user_traits = event_options.user_traits
|
29
|
+
@request = RequestContext.new(cid: client_token ? client_token.cid : '', vid: client_token ? client_token.vid : '',
|
30
|
+
fp: client_token ? client_token.fp : '', ip: @context.ip,
|
31
|
+
remote_ip: @context.remote_ip, headers: @context.headers,
|
32
|
+
url: @context.url, http_method: @context.http_method)
|
33
|
+
|
34
|
+
|
35
|
+
@timestamp = SecureNative::Utils::DateUtils.to_timestamp(event_options.timestamp)
|
36
|
+
@properties = event_options.properties
|
37
|
+
end
|
38
|
+
|
39
|
+
def to_s
|
40
|
+
"context: #{@context}, rid: #{@rid}, event_type: #{@event_type}, user_id: #{@user_id},
|
41
|
+
user_traits: #{@user_traits}, request: #{@request}, timestamp: #{@timestamp}, properties: #{@properties}"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SecureNative
|
4
|
+
class UserTraits
|
5
|
+
attr_reader :name, :email, :phone, :created_at
|
6
|
+
attr_writer :name, :email, :phone, :created_at
|
7
|
+
|
8
|
+
def initialize(name: nil, email: nil, phone: nil, created_at: nil)
|
9
|
+
@name = name
|
10
|
+
@email = email
|
11
|
+
@created_at = created_at
|
12
|
+
@phone = phone
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SecureNative
|
4
|
+
module Utils
|
5
|
+
class EncryptionUtils
|
6
|
+
def self.padding_key(key, length)
|
7
|
+
if key.length == length
|
8
|
+
key
|
9
|
+
else
|
10
|
+
if key.length > length
|
11
|
+
key.slice(0, length)
|
12
|
+
else
|
13
|
+
(length - key.length).times { key << '0' }
|
14
|
+
key
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.encrypt(plain_text, secret_key)
|
20
|
+
begin
|
21
|
+
cipher = OpenSSL::Cipher.new('aes-256-cbc')
|
22
|
+
cipher.encrypt
|
23
|
+
iv = cipher.random_iv
|
24
|
+
cipher.key = padding_key(secret_key, 32)
|
25
|
+
encrypted = cipher.update(plain_text) + cipher.final
|
26
|
+
(iv + encrypted).unpack1('H*')
|
27
|
+
rescue StandardError
|
28
|
+
''
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.decrypt(cipher_text, secret_key)
|
33
|
+
begin
|
34
|
+
cipher = OpenSSL::Cipher.new('aes-256-cbc')
|
35
|
+
cipher.decrypt
|
36
|
+
raw_data = [cipher_text].pack('H*')
|
37
|
+
cipher.iv = raw_data.slice(0, 16)
|
38
|
+
cipher.key = padding_key(secret_key, 32)
|
39
|
+
decrypted = JSON.parse(cipher.update(raw_data.slice(16, raw_data.length)) + cipher.final)
|
40
|
+
|
41
|
+
SecureNative::ClientToken.new(decrypted['cid'], decrypted['vid'], decrypted['fp'])
|
42
|
+
rescue StandardError
|
43
|
+
SecureNative::ClientToken.new('', '', '')
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|