securenative 0.1.5 → 0.1.16
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 +57 -0
- data/.github/workflows/publish.yml +60 -0
- data/.github/workflows/test.yml +45 -0
- data/.rakeTasks +7 -0
- data/.rspec +3 -0
- data/.travis.yml +6 -0
- data/Gemfile +4 -1
- data/Gemfile.lock +28 -2
- data/README.md +134 -66
- data/Rakefile +5 -1
- data/VERSION +1 -0
- data/lib/securenative/api_manager.rb +30 -0
- data/lib/securenative/config/configuration_builder.rb +26 -0
- data/lib/securenative/config/configuration_manager.rb +53 -0
- data/lib/securenative/config/securenative_options.rb +18 -0
- data/lib/securenative/context/context_builder.rb +59 -0
- data/lib/securenative/context/securenative_context.rb +14 -0
- data/lib/securenative/enums/api_route.rb +4 -0
- data/lib/securenative/enums/event_types.rb +21 -0
- data/lib/securenative/enums/failover_strategy.rb +4 -0
- data/lib/securenative/enums/risk_level.rb +5 -0
- data/lib/securenative/event_manager.rb +122 -61
- data/lib/securenative/event_options_builder.rb +21 -0
- data/lib/securenative/exceptions/securenative_config_exception.rb +2 -0
- data/lib/securenative/exceptions/securenative_http_exception.rb +2 -0
- data/lib/securenative/exceptions/securenative_invalid_options_exception.rb +2 -0
- data/lib/securenative/exceptions/securenative_invalid_uri_exception.rb +2 -0
- data/lib/securenative/exceptions/securenative_parse_exception.rb +2 -0
- data/lib/securenative/exceptions/securenative_sdk_Illegal_state_exception.rb +2 -0
- data/lib/securenative/exceptions/securenative_sdk_exception.rb +2 -0
- data/lib/securenative/http/http_response.rb +10 -0
- data/lib/securenative/http/securenative_http_client.rb +30 -0
- data/lib/securenative/logger.rb +42 -0
- data/lib/securenative/models/client_token.rb +10 -0
- data/lib/securenative/models/device.rb +8 -0
- data/lib/securenative/models/event_options.rb +13 -0
- data/lib/securenative/models/request_context.rb +15 -0
- data/lib/securenative/models/request_options.rb +10 -0
- data/lib/securenative/models/sdk_event.rb +25 -0
- data/lib/securenative/models/user_traits.rb +10 -0
- data/lib/securenative/models/verify_result.rb +10 -0
- data/lib/securenative/securenative.iml +9 -0
- data/lib/securenative/securenative.rb +82 -0
- data/lib/securenative/utils/date_utils.rb +7 -0
- data/lib/securenative/utils/encryption_utils.rb +38 -0
- data/lib/securenative/utils/ip_utils.rb +22 -0
- data/lib/securenative/utils/request_utils.rb +21 -0
- data/lib/securenative/utils/signature_utils.rb +14 -0
- data/lib/securenative/utils/utils.rb +9 -0
- data/lib/securenative/utils/version_utils.rb +10 -0
- data/securenative.gemspec +4 -4
- metadata +51 -15
- data/lib/securenative.rb +0 -39
- data/lib/securenative/config.rb +0 -9
- data/lib/securenative/event_options.rb +0 -86
- data/lib/securenative/event_type.rb +0 -21
- data/lib/securenative/http_client.rb +0 -20
- data/lib/securenative/secure_native_sdk.rb +0 -62
- data/lib/securenative/securenative_options.rb +0 -17
- data/lib/securenative/sn_exception.rb +0 -5
- data/lib/securenative/utils.rb +0 -41
data/Rakefile
CHANGED
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.16
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'json'
|
2
|
+
require_relative 'logger'
|
3
|
+
|
4
|
+
class ApiManager
|
5
|
+
def initialize(event_manager, securenative_options)
|
6
|
+
@event_manager = event_manager
|
7
|
+
@options = securenative_options
|
8
|
+
end
|
9
|
+
|
10
|
+
def track(event_options)
|
11
|
+
Logger.debug('Track event call')
|
12
|
+
event = SDKEvent.new(event_options, @options)
|
13
|
+
@event_manager.send_async(event, ApiRoute::TRACK)
|
14
|
+
end
|
15
|
+
|
16
|
+
def verify(event_options)
|
17
|
+
Logger.debug('Verify event call')
|
18
|
+
event = SDKEvent.new(event_options, @options)
|
19
|
+
|
20
|
+
begin
|
21
|
+
res = JSON.parse(@event_manager.send_sync(event, ApiRoute::VERIFY, false))
|
22
|
+
return VerifyResult.new(res['riskLevel'], res['score'], res['triggers'])
|
23
|
+
rescue StandardError => e
|
24
|
+
Logger.debug('Failed to call verify; {}'.format(e))
|
25
|
+
end
|
26
|
+
return VerifyResult.new(RiskLevel::LOW, 0, nil) if @options.fail_over_strategy == FailOverStrategy::FAIL_OPEN
|
27
|
+
|
28
|
+
VerifyResult.new(RiskLevel::HIGH, 1, nil)
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
class ConfigurationBuilder
|
2
|
+
attr_reader :api_key, :api_url, :interval, :max_events, :timeout, :auto_send, :disable, :log_level, :fail_over_strategy
|
3
|
+
attr_writer :api_key, :api_url, :interval, :max_events, :timeout, :auto_send, :disable, :log_level, :fail_over_strategy
|
4
|
+
|
5
|
+
def initialize(api_key = nil, api_url = 'https://api.securenative.com/collector/api/v1', interval = 1000,
|
6
|
+
max_events = 1000, timeout = 1500, auto_send = true, disable = false, log_level = 'FATAL',
|
7
|
+
fail_over_strategy = FailOverStrategy::FAIL_OPEN)
|
8
|
+
@api_key = api_key
|
9
|
+
@api_url = api_url
|
10
|
+
@interval = interval
|
11
|
+
@max_events = max_events
|
12
|
+
@timeout = timeout
|
13
|
+
@auto_send = auto_send
|
14
|
+
@disable = disable
|
15
|
+
@log_level = log_level
|
16
|
+
@fail_over_strategy = fail_over_strategy
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.default_config_builder
|
20
|
+
ConfigurationBuilder()
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.default_securenative_options
|
24
|
+
SecureNativeOptions()
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'parseconfig'
|
2
|
+
|
3
|
+
class ConfigurationManager
|
4
|
+
DEFAULT_CONFIG_FILE = 'securenative.cfg'.freeze
|
5
|
+
CUSTOM_CONFIG_FILE_ENV_NAME = 'SECURENATIVE_COMFIG_FILE'.freeze
|
6
|
+
@config = nil
|
7
|
+
|
8
|
+
def self.read_resource_file(resource_path)
|
9
|
+
@config = ParseConfig.new(resource_path)
|
10
|
+
|
11
|
+
properties = {}
|
12
|
+
@config.get_groups.each { |group|
|
13
|
+
group.each do |key, value|
|
14
|
+
properties[key.upcase] = value
|
15
|
+
end
|
16
|
+
}
|
17
|
+
properties
|
18
|
+
end
|
19
|
+
|
20
|
+
def self._get_resource_path(env_name)
|
21
|
+
Env.fetch(env_name, ENV[DEFAULT_CONFIG_FILE])
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.config_builder
|
25
|
+
ConfigurationBuilder.default_config_builder
|
26
|
+
end
|
27
|
+
|
28
|
+
def self._get_env_or_default(properties, key, default)
|
29
|
+
return Env[key] if Env[key]
|
30
|
+
return properties[key] if properties[key]
|
31
|
+
|
32
|
+
default
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.load_config
|
36
|
+
options = ConfigurationBuilder().default_securenative_options
|
37
|
+
|
38
|
+
resource_path = DEFAULT_CONFIG_FILE
|
39
|
+
resource_path = Env[CUSTOM_CONFIG_FILE_ENV_NAME] if Env[CUSTOM_CONFIG_FILE_ENV_NAME]
|
40
|
+
|
41
|
+
properties = read_resource_file(resource_path)
|
42
|
+
|
43
|
+
ConfigurationBuilder(_get_env_or_default(properties, 'SECURENATIVE_API_KEY', options.api_key),
|
44
|
+
_get_env_or_default(properties, 'SECURENATIVE_API_URL', options.api_url),
|
45
|
+
_get_env_or_default(properties, 'SECURENATIVE_INTERVAL', options.interval),
|
46
|
+
_get_env_or_default(properties, 'SECURENATIVE_MAX_EVENTS', options.max_events),
|
47
|
+
_get_env_or_default(properties, 'SECURENATIVE_TIMEOUT', options.timeout),
|
48
|
+
_get_env_or_default(properties, 'SECURENATIVE_AUTO_SEND', options.auto_send),
|
49
|
+
_get_env_or_default(properties, 'SECURENATIVE_DISABLE', options.disable),
|
50
|
+
_get_env_or_default(properties, 'SECURENATIVE_LOG_LEVEL', options.log_level),
|
51
|
+
_get_env_or_default(properties, 'SECURENATIVE_FAILOVER_STRATEGY', options.fail_over_strategy))
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
class SecureNativeOptions
|
2
|
+
attr_reader :api_key, :api_url, :interval, :max_events, :timeout, :auto_send, :disable, :log_level, :fail_over_strategy
|
3
|
+
attr_writer :api_key, :api_url, :interval, :max_events, :timeout, :auto_send, :disable, :log_level, :fail_over_strategy
|
4
|
+
|
5
|
+
def initialize(api_key = nil, api_url = "https://api.securenative.com/collector/api/v1", interval = 1000,
|
6
|
+
max_events = 1000, timeout = 1500, auto_send = true, disable = false, log_level = "FATAL",
|
7
|
+
fail_over_strategy = FailOverStrategy::FAIL_OPEN)
|
8
|
+
@api_key = api_key
|
9
|
+
@api_url = api_url
|
10
|
+
@interval = interval
|
11
|
+
@max_events = max_events
|
12
|
+
@timeout = timeout
|
13
|
+
@auto_send = auto_send
|
14
|
+
@disable = disable
|
15
|
+
@log_level = log_level
|
16
|
+
@fail_over_strategy = fail_over_strategy
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
class ContextBuilder
|
2
|
+
attr_reader :context
|
3
|
+
|
4
|
+
def initialize(client_token = nil, ip = nil, remote_ip = nil, headers = nil, url = nil, method = nil, body = nil)
|
5
|
+
@context = SecureNativeContext(client_token, ip, remote_ip, headers, url, method, body)
|
6
|
+
end
|
7
|
+
|
8
|
+
def client_token(client_token)
|
9
|
+
@context.client_token = client_token
|
10
|
+
end
|
11
|
+
|
12
|
+
def ip(ip)
|
13
|
+
@context.ip = ip
|
14
|
+
end
|
15
|
+
|
16
|
+
def remote_ip(remote_ip)
|
17
|
+
@context.remote_ip = remote_ip
|
18
|
+
end
|
19
|
+
|
20
|
+
def headers(headers)
|
21
|
+
@context.headers = headers
|
22
|
+
end
|
23
|
+
|
24
|
+
def url(url)
|
25
|
+
@context.url = url
|
26
|
+
end
|
27
|
+
|
28
|
+
def method(method)
|
29
|
+
@context.method = method
|
30
|
+
end
|
31
|
+
|
32
|
+
def body(body)
|
33
|
+
@context.body = body
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.default_context_builder
|
37
|
+
ContextBuilder()
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.from_http_request(request)
|
41
|
+
begin
|
42
|
+
client_token = request.cookies[RequestUtils.SECURENATIVE_COOKIE]
|
43
|
+
rescue StandardError
|
44
|
+
client_token = nil
|
45
|
+
end
|
46
|
+
|
47
|
+
begin
|
48
|
+
headers = request.headers
|
49
|
+
rescue StandardError
|
50
|
+
headers = nil
|
51
|
+
end
|
52
|
+
|
53
|
+
client_token = RequestUtils.get_secure_header_from_request(headers) if Utils.null_or_empty?(client_token)
|
54
|
+
|
55
|
+
ContextBuilder(url = request.url, method = request.method, header = headers, client_token = client_token,
|
56
|
+
client_ip = RequestUtils.get_client_ip_from_request(request),
|
57
|
+
remote_ip = RequestUtils.get_remote_ip_from_request(request), nil)
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
class SecureNativeContext
|
2
|
+
attr_reader :client_token, :ip, :remote_ip, :headers, :url, :method, :body
|
3
|
+
attr_writer :client_token, :ip, :remote_ip, :headers, :url, :method, :body
|
4
|
+
|
5
|
+
def initialize(client_token = nil, ip = nil, remote_ip = nil, headers = nil, url = nil, method = nil, body = nil)
|
6
|
+
@client_token = client_token
|
7
|
+
@ip = ip
|
8
|
+
@remote_ip = remote_ip
|
9
|
+
@headers = headers
|
10
|
+
@url = url
|
11
|
+
@method = method
|
12
|
+
@body = body
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module EventTypes
|
2
|
+
LOG_IN = 'sn.user.login'.freeze
|
3
|
+
LOG_IN_CHALLENGE = 'sn.user.login.challenge'.freeze
|
4
|
+
LOG_IN_FAILURE = 'sn.user.login.failure'.freeze
|
5
|
+
LOG_OUT = 'sn.user.logout'.freeze
|
6
|
+
SIGN_UP = 'sn.user.signup'.freeze
|
7
|
+
AUTH_CHALLENGE = 'sn.user.auth.challenge'.freeze
|
8
|
+
AUTH_CHALLENGE_SUCCESS = 'sn.user.auth.challenge.success'.freeze
|
9
|
+
AUTH_CHALLENGE_FAILURE = 'sn.user.auth.challenge.failure'.freeze
|
10
|
+
TWO_FACTOR_DISABLE = 'sn.user.2fa.disable'.freeze
|
11
|
+
EMAIL_UPDATE = 'sn.user.email.update'.freeze
|
12
|
+
PASSWORD_REST = 'sn.user.password.reset'.freeze
|
13
|
+
PASSWORD_REST_SUCCESS = 'sn.user.password.reset.success'.freeze
|
14
|
+
PASSWORD_UPDATE = 'sn.user.password.update'.freeze
|
15
|
+
PASSWORD_REST_FAILURE = 'sn.user.password.reset.failure'.freeze
|
16
|
+
USER_INVITE = 'sn.user.invite'.freeze
|
17
|
+
ROLE_UPDATE = 'sn.user.role.update'.freeze
|
18
|
+
PROFILE_UPDATE = 'sn.user.profile.update'.freeze
|
19
|
+
PAGE_VIEW = 'sn.user.page.view'.freeze
|
20
|
+
VERIFY = 'sn.verify'.freeze
|
21
|
+
end
|
@@ -1,88 +1,149 @@
|
|
1
|
-
require_relative '
|
2
|
-
require_relative 'http_client'
|
3
|
-
require_relative 'sn_exception'
|
4
|
-
require 'json'
|
5
|
-
require 'thread'
|
1
|
+
require_relative 'logger'
|
6
2
|
|
7
3
|
class QueueItem
|
8
|
-
|
4
|
+
attr_reader :url, :body, :retry
|
5
|
+
attr_writer :url, :body, :retry
|
6
|
+
|
7
|
+
def initialize(url, body, _retry)
|
9
8
|
@url = url
|
10
9
|
@body = body
|
10
|
+
@retry = _retry
|
11
11
|
end
|
12
|
-
|
13
|
-
attr_reader :url
|
14
|
-
attr_reader :body
|
15
12
|
end
|
16
13
|
|
17
14
|
class EventManager
|
18
|
-
def initialize(
|
19
|
-
if api_key
|
20
|
-
raise SecureNativeSDKException.
|
15
|
+
def initialize(options = SecureNativeOptions(), http_client = nil)
|
16
|
+
if options.api_key.nil?
|
17
|
+
raise SecureNativeSDKException('API key cannot be None, please get your API key from SecureNative console.')
|
21
18
|
end
|
22
19
|
|
23
|
-
@
|
20
|
+
@http_client = if http_client.nil?
|
21
|
+
SecureNativeHttpClient(options)
|
22
|
+
else
|
23
|
+
http_client
|
24
|
+
end
|
25
|
+
|
26
|
+
@queue = []
|
27
|
+
@thread = Thread.new(run)
|
28
|
+
@thread.start
|
29
|
+
|
24
30
|
@options = options
|
25
|
-
@
|
26
|
-
@
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
Thread.new do
|
31
|
-
loop do
|
32
|
-
flush
|
33
|
-
sleep(interval_seconds)
|
34
|
-
end
|
35
|
-
end
|
36
|
-
end
|
31
|
+
@send_enabled = false
|
32
|
+
@attempt = 0
|
33
|
+
@coefficients = [1, 1, 2, 3, 5, 8, 13]
|
34
|
+
@thread = nil
|
35
|
+
@interval = options.interval
|
37
36
|
end
|
38
37
|
|
39
|
-
def send_async(event,
|
40
|
-
|
41
|
-
|
42
|
-
|
38
|
+
def send_async(event, resource_path)
|
39
|
+
if @options.disable
|
40
|
+
Logger.warning('SDK is disabled. no operation will be performed')
|
41
|
+
return
|
42
|
+
end
|
43
43
|
|
44
|
-
|
45
|
-
@
|
46
|
-
build_url(path),
|
47
|
-
@api_key,
|
48
|
-
event.to_hash.to_json
|
49
|
-
)
|
44
|
+
item = QueueItem(resource_path, JSON.parse(EventManager.serialize(event)), false)
|
45
|
+
@queue.append(item)
|
50
46
|
end
|
51
47
|
|
52
48
|
def flush
|
53
|
-
|
54
|
-
|
55
|
-
while i >= 0
|
56
|
-
item = @queue.pop
|
57
|
-
@http_client.post(
|
58
|
-
build_url(item.url),
|
59
|
-
@api_key,
|
60
|
-
item.body.to_hash.to_json
|
61
|
-
)
|
62
|
-
end
|
63
|
-
else
|
64
|
-
q = Array.new(@queue.size) {@queue.pop}
|
65
|
-
q.each do |item|
|
66
|
-
@http_client.post(
|
67
|
-
build_url(item.url),
|
68
|
-
@api_key,
|
69
|
-
item.body
|
70
|
-
)
|
71
|
-
@queue = Queue.new
|
72
|
-
end
|
49
|
+
@queue.each do |item|
|
50
|
+
@http_client.post(item.url, item.body)
|
73
51
|
end
|
74
52
|
end
|
75
53
|
|
76
|
-
|
54
|
+
def send_sync(event, resource_path, _retry)
|
55
|
+
if @options.disable
|
56
|
+
Logger.warning('SDK is disabled. no operation will be performed')
|
57
|
+
return
|
58
|
+
end
|
59
|
+
|
60
|
+
Logger.debug('Attempting to send event {}'.format(event))
|
61
|
+
res = @http_client.post(resource_path, JSON.parse(EventManager.serialize(event)))
|
77
62
|
|
78
|
-
|
79
|
-
|
63
|
+
if res.status_code != 200
|
64
|
+
Logger.info('SecureNative failed to call endpoint {} with event {}. adding back to queue'.format(resource_path, event))
|
65
|
+
item = QueueItem(resource_path, JSON.parse(EventManager.serialize(event)), _retry)
|
66
|
+
@queue.append(item)
|
67
|
+
end
|
68
|
+
|
69
|
+
res
|
70
|
+
end
|
71
|
+
|
72
|
+
def run
|
73
|
+
loop do
|
74
|
+
next unless !@queue.empty? && @send_enabled
|
75
|
+
|
76
|
+
@queue.each do |item|
|
77
|
+
begin
|
78
|
+
res = @http_client.post(item.url, item.body)
|
79
|
+
if res.status_code == 401
|
80
|
+
item.retry = false
|
81
|
+
elsif res.status_code != 200
|
82
|
+
raise SecureNativeHttpException(res.status_code)
|
83
|
+
end
|
84
|
+
Logger.debug('Event successfully sent; {}'.format(item.body))
|
85
|
+
return res
|
86
|
+
rescue StandardError => e
|
87
|
+
Logger.error('Failed to send event; {}'.format(e))
|
88
|
+
if item.retry
|
89
|
+
@attempt = 0 if @coefficients.length == @attempt + 1
|
90
|
+
|
91
|
+
back_off = @coefficients[@attempt] * @options.interval
|
92
|
+
Logger.debug('Automatic back-off of {}'.format(back_off))
|
93
|
+
@send_enabled = false
|
94
|
+
sleep back_off
|
95
|
+
@send_enabled = true
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
sleep @interval / 1000
|
100
|
+
end
|
80
101
|
end
|
81
102
|
|
82
|
-
|
103
|
+
def start_event_persist
|
104
|
+
Logger.debug('Starting automatic event persistence')
|
105
|
+
if @options.auto_send || @send_enabled
|
106
|
+
@send_enabled = true
|
107
|
+
else
|
108
|
+
Logger.debug('Automatic event persistence is disabled, you should persist events manually')
|
109
|
+
end
|
110
|
+
end
|
83
111
|
|
84
|
-
def
|
85
|
-
|
112
|
+
def stop_event_persist
|
113
|
+
if @send_enabled
|
114
|
+
Logger.debug('Attempting to stop automatic event persistence')
|
115
|
+
begin
|
116
|
+
flush
|
117
|
+
@thread&.stop
|
118
|
+
Logger.debug('Stopped event persistence')
|
119
|
+
rescue StandardError => e
|
120
|
+
Logger.error('Could not stop event scheduler; {}'.format(e))
|
121
|
+
end
|
122
|
+
end
|
86
123
|
end
|
87
124
|
|
125
|
+
def self.serialize(obj)
|
126
|
+
{
|
127
|
+
rid: obj.rid,
|
128
|
+
eventType: obj.event_type,
|
129
|
+
userId: obj.user_id,
|
130
|
+
userTraits: {
|
131
|
+
name: obj.user_traits.name,
|
132
|
+
email: obj.user_traits.email,
|
133
|
+
createdAt: obj.user_traits.created_at
|
134
|
+
},
|
135
|
+
request: {
|
136
|
+
cid: obj.request.cid,
|
137
|
+
vid: obj.request.vid,
|
138
|
+
fp: obj.request.fp,
|
139
|
+
ip: obj.request.ip,
|
140
|
+
remoteIp: obj.request.remote_ip,
|
141
|
+
method: obj.request.method,
|
142
|
+
url: obj.request.url,
|
143
|
+
headers: obj.request.headers
|
144
|
+
},
|
145
|
+
timestamp: obj.timestamp,
|
146
|
+
properties: obj.properties
|
147
|
+
}
|
148
|
+
end
|
88
149
|
end
|