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
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
class EventOptionsBuilder
|
|
2
|
+
MAX_PROPERTIES_SIZE = 10
|
|
3
|
+
|
|
4
|
+
def initialize(event_type, user_id, user_traits, user_name, email, created_at, context, properties, timestamp)
|
|
5
|
+
@event_options = EventOptions(event_type)
|
|
6
|
+
@event_options.user_id = user_id
|
|
7
|
+
@event_options.user_traits = user_traits if user_traits
|
|
8
|
+
@event_options.user_traits = UserTraits(name, email, created_at) if user_name && email && created_at
|
|
9
|
+
@event_options.context = context
|
|
10
|
+
@event_options.properties = properties
|
|
11
|
+
@event_options.timestamp = timestamp
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def build
|
|
15
|
+
if !@event_options.properties.nil? && @event_options.properties.length > MAX_PROPERTIES_SIZE
|
|
16
|
+
raise SecureNativeInvalidOptionsException('You can have only up to {} custom properties', MAX_PROPERTIES_SIZE)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
@event_options
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
require 'httpclient'
|
|
2
|
+
|
|
3
|
+
class SecureNativeHttpClient
|
|
4
|
+
AUTHORIZATION_HEADER = 'Authorization'.freeze
|
|
5
|
+
VERSION_HEADER = 'SN-Version'.freeze
|
|
6
|
+
USER_AGENT_HEADER = 'User-Agent'.freeze
|
|
7
|
+
USER_AGENT_HEADER_VALUE = 'SecureNative-python'.freeze
|
|
8
|
+
CONTENT_TYPE_HEADER = 'Content-Type'.freeze
|
|
9
|
+
CONTENT_TYPE_HEADER_VALUE = 'application/json'.freeze
|
|
10
|
+
|
|
11
|
+
def __init__(securenative_options)
|
|
12
|
+
@options = securenative_options
|
|
13
|
+
@client = HTTPClient.new
|
|
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 => VersionUtils.version,
|
|
21
|
+
AUTHORIZATION_HEADER => options.api_key
|
|
22
|
+
}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def post(path, body)
|
|
26
|
+
url = '{}/{}'.format(@options.api_url, path)
|
|
27
|
+
headers = _headers
|
|
28
|
+
@client.post(url, body, headers)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
require 'logger'
|
|
2
|
+
|
|
3
|
+
class Logger
|
|
4
|
+
@logger = Logger.new(STDOUT)
|
|
5
|
+
|
|
6
|
+
def self.init_logger(level)
|
|
7
|
+
@logger.level = case level
|
|
8
|
+
when 'WARN'
|
|
9
|
+
Logger::WARN
|
|
10
|
+
when 'DEBUG'
|
|
11
|
+
Logger::DEBUG
|
|
12
|
+
when 'ERROR'
|
|
13
|
+
Logger::ERROR
|
|
14
|
+
when 'FATAL'
|
|
15
|
+
Logger::FATAL
|
|
16
|
+
when 'INFO'
|
|
17
|
+
Logger::INFO
|
|
18
|
+
else
|
|
19
|
+
Logger::FATAL
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
@logger.formatter = proc do |severity, datetime, progname, msg|
|
|
23
|
+
"[#{datetime}] #{severity} (#{progname}): #{msg}\n"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.info(msg)
|
|
28
|
+
@logger.info(msg)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.debug(msg)
|
|
32
|
+
@logger.debug(msg)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.warning(msg)
|
|
36
|
+
@logger.warning(msg)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.error(msg)
|
|
40
|
+
@logger.error(msg)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
class EventOptions
|
|
2
|
+
attr_reader :event, :user_id, :user_traits, :context, :properties, :timestamp
|
|
3
|
+
attr_writer :event, :user_id, :user_traits, :context, :properties, :timestamp
|
|
4
|
+
|
|
5
|
+
def initialize(event, user_id = nil, user_traits = nil, context = nil, properties = nil, timestamp = nil)
|
|
6
|
+
@event = event
|
|
7
|
+
@user_id = user_id
|
|
8
|
+
@user_traits = user_traits
|
|
9
|
+
@context = context
|
|
10
|
+
@properties = properties
|
|
11
|
+
@timestamp = timestamp
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
class RequestContext
|
|
2
|
+
attr_reader :cid, :vid, :fp, :ip, :remote_ip, :headers, :url, :method
|
|
3
|
+
attr_writer :cid, :vid, :fp, :ip, :remote_ip, :headers, :url, :method
|
|
4
|
+
|
|
5
|
+
def initialize(cid = nil, vid = nil, fp = nil, ip = nil, remote_ip = nil, headers = nil, url = nil, method = nil)
|
|
6
|
+
@cid = cid
|
|
7
|
+
@vid = vid
|
|
8
|
+
@fp = fp
|
|
9
|
+
@ip = ip
|
|
10
|
+
@remote_ip = remote_ip
|
|
11
|
+
@headers = headers
|
|
12
|
+
@url = url
|
|
13
|
+
@method = method
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
class SDKEvent
|
|
2
|
+
attr_reader :context, :rid, :event_type, :user_id, :user_traits, :request, :timestamp, :properties
|
|
3
|
+
attr_writer :context, :rid, :event_type, :user_id, :user_traits, :request, :timestamp, :properties
|
|
4
|
+
|
|
5
|
+
def initialize(event_options, securenative_options)
|
|
6
|
+
@context = if !event_options.context.nil?
|
|
7
|
+
event_options.context
|
|
8
|
+
else
|
|
9
|
+
ContextBuilder.default_context_builder
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
client_token = EncryptionUtils.decrypt(@context.client_token, securenative_options.api_key)
|
|
13
|
+
|
|
14
|
+
@rid = SecureRandom.uuid.to_str
|
|
15
|
+
@event_type = event_options.event
|
|
16
|
+
@user_id = event_options.user_id
|
|
17
|
+
@user_traits = event_options.user_traits
|
|
18
|
+
@request = RequestContext(cid = client_token ? client_token.cid : '', vid = client_token ? client_token.vid : '',
|
|
19
|
+
fp = client_token ? client_token.fp : '', ip = @context.ip, remote_ip = @context.remote_ip,
|
|
20
|
+
method = @context.method, url = @context.url, headers = @context.headers)
|
|
21
|
+
|
|
22
|
+
@timestamp = DateUtils.to_timestamp(event_options.timestamp)
|
|
23
|
+
@properties = event_options.properties
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<module type="RUBY_MODULE" version="4">
|
|
3
|
+
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
|
4
|
+
<exclude-output />
|
|
5
|
+
<content url="file://$MODULE_DIR$" />
|
|
6
|
+
<orderEntry type="jdk" jdkName="ruby-2.7.1-p83" jdkType="RUBY_SDK" />
|
|
7
|
+
<orderEntry type="sourceFolder" forTests="false" />
|
|
8
|
+
</component>
|
|
9
|
+
</module>
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
require_relative 'logger'
|
|
2
|
+
require_relative 'utils/signature_utils'
|
|
3
|
+
|
|
4
|
+
class SecureNative
|
|
5
|
+
attr_reader :options
|
|
6
|
+
|
|
7
|
+
def initialize(options)
|
|
8
|
+
@securenative = nil
|
|
9
|
+
raise SecureNativeSDKException('You must pass your SecureNative api key') if Utils.null_or_empty?(options.api_key)
|
|
10
|
+
|
|
11
|
+
@options = options
|
|
12
|
+
@event_manager = EventManager(@options)
|
|
13
|
+
|
|
14
|
+
@event_manager.start_event_persist unless @options.api_url.nil?
|
|
15
|
+
|
|
16
|
+
@api_manager = ApiManager.new(@event_manager, @options)
|
|
17
|
+
Logger.init_logger(@options.log_level)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.init_with_options(options)
|
|
21
|
+
if @securenative.nil?
|
|
22
|
+
@securenative = SecureNative.new(options)
|
|
23
|
+
@securenative
|
|
24
|
+
else
|
|
25
|
+
Logger.debug('This SDK was already initialized.')
|
|
26
|
+
raise SecureNativeSDKException('This SDK was already initialized.')
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.init_with_api_key(api_key)
|
|
31
|
+
raise SecureNativeConfigException('You must pass your SecureNative api key') if Utils.null_or_empty?(api_key)
|
|
32
|
+
|
|
33
|
+
if @securenative.nil?
|
|
34
|
+
options = ConfigurationBuilder(api_key = api_key)
|
|
35
|
+
@securenative = SecureNative.new(options)
|
|
36
|
+
@securenative
|
|
37
|
+
else
|
|
38
|
+
Logger.debug('This SDK was already initialized.')
|
|
39
|
+
raise SecureNativeSDKException(u('This SDK was already initialized.'))
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.init
|
|
44
|
+
options = ConfigurationManager.load_config
|
|
45
|
+
init_with_options(options)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.instance
|
|
49
|
+
raise SecureNativeSDKIllegalStateException() if @securenative.nil?
|
|
50
|
+
|
|
51
|
+
@securenative
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def self.config_builder(api_key = nil, api_url = 'https://api.securenative.com/collector/api/v1', interval = 1000,
|
|
55
|
+
max_events = 1000, timeout = 1500, auto_send = true, disable = false, log_level = 'FATAL',
|
|
56
|
+
fail_over_strategy = FailOverStrategy::FAIL_OPEN)
|
|
57
|
+
ConfigurationBuilder(api_key, api_url, interval, max_events, timeout, auto_send, disable, log_level, fail_over_strategy)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def self.context_builder(client_token = nil, ip = nil, remote_ip = nil, headers = nil, url = nil, method = nil, body = nil)
|
|
61
|
+
ContextBuilder(client_token, ip, remote_ip, headers, url, method, body)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def track(event_options)
|
|
65
|
+
@api_manager.track(event_options)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def verify(event_options)
|
|
69
|
+
@api_manager.verify(event_options)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def self._flush
|
|
73
|
+
@securenative = nil
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def verify_request_payload(request)
|
|
77
|
+
request_signature = request.header[SignatureUtils.SIGNATURE_HEADER]
|
|
78
|
+
body = request.body
|
|
79
|
+
|
|
80
|
+
SignatureUtils.valid_signature?(@options.api_key, body, request_signature)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'openssl'
|
|
4
|
+
|
|
5
|
+
class EncryptionUtils
|
|
6
|
+
BLOCK_SIZE = 16
|
|
7
|
+
KEY_SIZE = 32
|
|
8
|
+
|
|
9
|
+
def self.encrypt(text, cipher_key)
|
|
10
|
+
cipher = OpenSSL::Cipher::AES.new(KEY_SIZE, :CBC).encrypt
|
|
11
|
+
cipher.padding = 0
|
|
12
|
+
|
|
13
|
+
if text.size % BLOCK_SIZE != 0
|
|
14
|
+
logger = Logger.new(STDOUT)
|
|
15
|
+
logger.level = Logger::WARN
|
|
16
|
+
logger.fatal('data not multiple of block length')
|
|
17
|
+
return nil
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
cipher_key = Digest::SHA1.hexdigest cipher_key
|
|
21
|
+
cipher.key = cipher_key.slice(0, BLOCK_SIZE)
|
|
22
|
+
s = cipher.update(text) + cipher.final
|
|
23
|
+
|
|
24
|
+
s.unpack('H*')[0].upcase
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.decrypt(encrypted, cipher_key)
|
|
28
|
+
cipher = OpenSSL::Cipher::AES.new(KEY_SIZE, :CBC).decrypt
|
|
29
|
+
cipher.padding = 0
|
|
30
|
+
|
|
31
|
+
cipher_key = Digest::SHA1.hexdigest cipher_key
|
|
32
|
+
cipher.key = cipher_key.slice(0, BLOCK_SIZE)
|
|
33
|
+
s = [encrypted].pack('H*').unpack('C*').pack('c*')
|
|
34
|
+
|
|
35
|
+
rv = cipher.update(s) + cipher.final
|
|
36
|
+
rv.strip
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
class IpUtils
|
|
2
|
+
VALID_IPV4_PATTERN = '(([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.){3}([01]?\\d\\d?|2[0-4]\\d|25[0-5])'.freeze
|
|
3
|
+
VALID_IPV6_PATTERN = '([0-9a-f]{1,4}:){7}([0-9a-f]){1,4}'.freeze
|
|
4
|
+
|
|
5
|
+
def self.ip_address?(ip_address)
|
|
6
|
+
return true if IpUtils.VALID_IPV4_PATTERN.match(ip_address)
|
|
7
|
+
return true if IpUtils.VALID_IPV6_PATTERN.match(ip_address)
|
|
8
|
+
|
|
9
|
+
false
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.valid_public_ip?(ip_address)
|
|
13
|
+
ip = IPAddr.new(ip_address)
|
|
14
|
+
return false if ip.loopback? || ip.private? || ip.link_local? || ip.untrusted? || ip.tainted?
|
|
15
|
+
|
|
16
|
+
true
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.loop_back?(ip_address)
|
|
20
|
+
IPAddr.new(ip_address).loopback?
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
class RequestUtils
|
|
2
|
+
SECURENATIVE_COOKIE = '_sn'.freeze
|
|
3
|
+
SECURENATIVE_HEADER = 'x-securenative'.freeze
|
|
4
|
+
|
|
5
|
+
def self.get_secure_header_from_request(headers)
|
|
6
|
+
return headers[RequestUtils.SECURENATIVE_HEADER] unless headers.nil?
|
|
7
|
+
|
|
8
|
+
[]
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.get_client_ip_from_request(request)
|
|
12
|
+
x_forwarded_for = request.env['HTTP_X_FORWARDED_FOR']
|
|
13
|
+
return x_forwarded_for unless x_forwarded_for.nil?
|
|
14
|
+
|
|
15
|
+
request.env['REMOTE_ADDR']
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.get_remote_ip_from_request(request)
|
|
19
|
+
request.remote_ip
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
require 'openssl'
|
|
2
|
+
|
|
3
|
+
class SignatureUtils
|
|
4
|
+
SIGNATURE_HEADER = 'x-securenative'.freeze
|
|
5
|
+
|
|
6
|
+
def self.valid_signature?(api_key, payload, header_signature)
|
|
7
|
+
key = api_key.encode('utf-8')
|
|
8
|
+
body = payload.encode('utf-8')
|
|
9
|
+
calculated_signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha512'), key, body)
|
|
10
|
+
calculated_signature.eql? header_signature
|
|
11
|
+
rescue StandardError
|
|
12
|
+
false
|
|
13
|
+
end
|
|
14
|
+
end
|