diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..009ea3d
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,57 @@
+name: CI
+
+on:
+ pull_request:
+ branches:
+ - master
+ - dev
+ - dev-*
+
+jobs:
+ ci:
+ runs-on: ubuntu-latest
+ name: CI
+ steps:
+ - name: Notify slack success
+ if: success()
+ id: slack # IMPORTANT: reference this step ID value in future Slack steps
+ env:
+ SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
+ uses: voxmedia/github-action-slack-notify-build@v1.1.1
+ with:
+ channel: github-actions
+ status: STARTING
+ color: warning
+
+ - uses: actions/checkout@v1
+ - uses: actions/setup-ruby@v1
+ with:
+ ruby-version: 2.6.x
+ - name: Install dependencies
+ run: |
+ gem install bundler
+ bundler install
+ - name: Run tests
+ run: bundler exec rspec
+
+ - name: Notify slack success
+ if: success()
+ env:
+ SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
+ uses: voxmedia/github-action-slack-notify-build@v1.1.1
+ with:
+ message_id: ${{ steps.slack.outputs.message_id }}
+ channel: github-actions
+ status: SUCCESS
+ color: good
+
+ - name: Notify slack fail
+ if: failure()
+ env:
+ SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
+ uses: voxmedia/github-action-slack-notify-build@v1.1.1
+ with:
+ message_id: ${{ steps.slack.outputs.message_id }}
+ channel: github-actions
+ status: FAILED
+ color: danger
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
new file mode 100644
index 0000000..2bec702
--- /dev/null
+++ b/.github/workflows/publish.yml
@@ -0,0 +1,60 @@
+name: Upload Python Package
+
+on:
+ release:
+ types: [created]
+
+jobs:
+ deploy:
+ name: Publish
+ runs-on: ubuntu-latest
+ steps:
+ - name: Notify slack success
+ if: success()
+ id: slack # IMPORTANT: reference this step ID value in future Slack steps
+ env:
+ SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
+ uses: voxmedia/github-action-slack-notify-build@v1.1.1
+ with:
+ channel: github-actions
+ status: STARTING
+ color: warning
+
+ - uses: actions/checkout@v2
+ - name: Set up Ruby 2.6
+ uses: actions/setup-ruby@v1
+ with:
+ version: 2.6.x
+
+ - name: Publish to RubyGems
+ run: |
+ mkdir -p $HOME/.gem
+ touch $HOME/.gem/credentials
+ chmod 0600 $HOME/.gem/credentials
+ printf -- "---\n:rubygems_api_key: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials
+ gem build *.gemspec
+ gem push *.gem
+ env:
+ GEM_HOST_API_KEY: ${{secrets.RUBYGEMS_AUTH_TOKEN}}
+
+ - name: Notify slack success
+ if: success()
+ env:
+ SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
+ uses: voxmedia/github-action-slack-notify-build@v1.1.1
+ with:
+ message_id: ${{ steps.slack.outputs.message_id }}
+ channel: github-actions
+ status: SUCCESS
+ color: good
+
+ - name: Notify slack fail
+ if: failure()
+ env:
+ SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
+ uses: voxmedia/github-action-slack-notify-build@v1.1.1
+ with:
+ message_id: ${{ steps.slack.outputs.message_id }}
+ channel: github-actions
+ status: FAILED
+ color: danger
\ No newline at end of file
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..31475ca
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,45 @@
+name: Testing
+
+on:
+ push:
+ branches:
+ - '*'
+ - '!master'
+
+jobs:
+ test:
+ name: Testing
+ runs-on: ubuntu-18.04
+ steps:
+ - uses: actions/checkout@v1
+ - uses: actions/setup-ruby@v1
+ with:
+ ruby-version: 2.6.x
+ - name: Install dependencies
+ run: |
+ gem install bundler
+ bundler install
+ - name: Run tests
+ run: bundler exec rspec
+
+ - name: Notify slack success
+ if: success()
+ env:
+ SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
+ uses: voxmedia/github-action-slack-notify-build@v1.1.1
+ with:
+ message_id: ${{ steps.slack.outputs.message_id }}
+ channel: github-actions
+ status: SUCCESS
+ color: good
+
+ - name: Notify slack fail
+ if: failure()
+ env:
+ SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
+ uses: voxmedia/github-action-slack-notify-build@v1.1.1
+ with:
+ message_id: ${{ steps.slack.outputs.message_id }}
+ channel: github-actions
+ status: FAILED
+ color: danger
\ No newline at end of file
diff --git a/.rakeTasks b/.rakeTasks
new file mode 100644
index 0000000..c6865d9
--- /dev/null
+++ b/.rakeTasks
@@ -0,0 +1,7 @@
+
+
diff --git a/.rspec b/.rspec
new file mode 100644
index 0000000..34c5164
--- /dev/null
+++ b/.rspec
@@ -0,0 +1,3 @@
+--format documentation
+--color
+--require spec_helper
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..8dd84f7
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,6 @@
+---
+language: ruby
+cache: bundler
+rvm:
+ - 2.7.1
+before_install: gem install bundler -v 2.1.2
diff --git a/Gemfile b/Gemfile
index 5914d4b..7b08844 100644
--- a/Gemfile
+++ b/Gemfile
@@ -3,4 +3,7 @@ source "https://rubygems.org"
gemspec
gem "rspec"
gem "rake"
-gem "httpclient"
\ No newline at end of file
+gem "httpclient"
+gem "parseconfig"
+gem "webmock"
+gem "codecov", :require => false, :group => :test
\ No newline at end of file
diff --git a/Gemfile.lock b/Gemfile.lock
index 4ee5fa5..1d3e55a 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,13 +1,26 @@
PATH
remote: .
specs:
- securenative (0.1.5)
+ securenative (0.1.16)
GEM
remote: https://rubygems.org/
specs:
+ addressable (2.7.0)
+ public_suffix (>= 2.0.2, < 5.0)
+ codecov (0.1.17)
+ json
+ simplecov
+ url
+ crack (0.4.3)
+ safe_yaml (~> 1.0.0)
diff-lcs (1.3)
+ docile (1.3.2)
+ hashdiff (1.0.1)
httpclient (2.8.3)
+ json (2.3.0)
+ parseconfig (1.0.8)
+ public_suffix (4.0.5)
rake (12.3.3)
rspec (3.8.0)
rspec-core (~> 3.8.0)
@@ -22,16 +35,29 @@ GEM
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.8.0)
rspec-support (3.8.2)
+ safe_yaml (1.0.5)
+ simplecov (0.18.5)
+ docile (~> 1.1)
+ simplecov-html (~> 0.11)
+ simplecov-html (0.12.2)
+ url (0.3.2)
+ webmock (3.8.3)
+ addressable (>= 2.3.6)
+ crack (>= 0.3.2)
+ hashdiff (>= 0.4.0, < 2.0.0)
PLATFORMS
ruby
DEPENDENCIES
bundler (~> 2.0)
+ codecov
httpclient
+ parseconfig
rake
rspec
securenative!
+ webmock
BUNDLED WITH
- 2.1.0.pre.1
+ 2.1.4
diff --git a/README.md b/README.md
index 909e2c1..f12c8a4 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,32 @@
-# SecureNative
-**[SecureNative](https://www.securenative.com/) is rethinking-security-as-a-service, disrupting the cyber security space and the way enterprises consume and implement security solutions.**
-
-## Installation
+
+
+
+
+
+ A Cloud-Native Security Monitoring and Protection for Modern Applications
+
+
+
+
+
+
+
+
+
+
+
+ Documentation |
+ Quick Start |
+ Blog |
+ Chat with us on Slack!
+
+
+
+
+[SecureNative](https://www.securenative.com/) performs user monitoring by analyzing user interactions with your application and various factors such as network, devices, locations and access patterns to stop and prevent account takeover attacks.
+
+
+## Install the SDK
Add this line to your application's Gemfile:
@@ -9,7 +34,7 @@ Add this line to your application's Gemfile:
gem 'securenative'
```
-And then execute:
+Then execute:
$ bundle
@@ -18,85 +43,128 @@ Or install it yourself as:
$ gem install securenative
## Initialize the SDK
-Retrieve your API key from settings page of your SecureNative account and use the following in your code to initialize and use the sdk.
+
+To get your *API KEY*, login to your SecureNative account and go to project settings page:
+
+### Option 1: Initialize via Config file
+SecureNative can automatically load your config from *securenative.cfg* file or from the file that is specified in your *SECURENATIVE_CONFIG_FILE* env variable:
+
```ruby
require 'securenative'
-# Do some cool stuff here
-# ...
-# ...
-
-begin
- SecureNative.init('YOUR_API_KEY') # Should be called before any other call to securenative
-rescue SecureNativeSDKException => e
- # Do some error handling
-end
+
+secureative = SecureNative.init
+```
+### Option 2: Initialize via API Key
+
+```ruby
+require 'securenative'
+
+
+securenative = SecureNative.init_with_api_key('YOUR_API_KEY')
+```
+
+### Option 3: Initialize via ConfigurationBuilder
+```ruby
+require 'securenative'
+
+
+securenative = SecureNative.init_with_options(SecureNative.config_builder(api_key = 'API_KEY', max_event = 10, log_level = 'ERROR'))
+```
+
+## Getting SecureNative instance
+Once initialized, sdk will create a singleton instance which you can get:
+```ruby
+require 'securenative'
+
+
+secureNative = SecureNative.instance
```
## Tracking events
-Once the SDK has been initialized, you can start sending new events with the `track` function:
+
+Once the SDK has been initialized, tracking requests sent through the SDK
+instance. Make sure you build event with the EventBuilder:
+
+ ```ruby
+require 'securenative'
+require 'securenative/enums/event_types'
+require 'securenative/event_options_builder'
+require 'securenative/models/user_traits'
+require 'securenative/context/context_builder'
+
+
+securenative = SecureNative.instance
+
+context = securenative.context_builder(ip = '127.0.0.1', client_token = 'SECURED_CLIENT_TOKEN',
+ headers = { 'user-agent' => 'Mozilla/5.0 (iPad; U; CPU OS 3_2_1 like Mac OS X; en-us) AppleWebKit/531.21.10 (KHTML, like Gecko) Mobile/7B405' })
+
+event_options = EventOptionsBuilder(event_type = EventTypes::LOG_IN,
+ user_id = 'USER_ID', user_traits = UserTraits('USER_NAME', 'USER_EMAIL'),
+ context = context, properties = {prop1 => 'CUSTOM_PARAM_VALUE', prop2 => true, prop3 => 3}).build
+
+securenative.track(event_options)
+ ```
+
+You can also create request context from requests:
+
```ruby
require 'securenative'
-require 'securenative/event_type'
-
-
-def login
- # Do some cool stuff here
- # ...
- # ...
-
- SecureNative.track(Event.new(
- event_type = EventTypes::LOG_IN,
- user: User.new("1", "Jon Snow", "jon@snow.com"),
- ip: "1.2.3.4",
- remote_ip: "5.6.7.8",
- user_agent: "Mozilla/5.0 (Linux; U; Android 4.4.2; zh-cn; GT-I9500 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko)Version/4.0 MQQBrowser/5.0 QQ-URL-Manager Mobile Safari/537.36",
- sn_cookie: "cookie"))
+require 'securenative/enums/event_types'
+require 'securenative/event_options_builder'
+require 'securenative/models/user_traits'
+
+
+def track(request)
+ securenative = SecureNative.instance
+ context = SecureNative.context_builder.from_http_request(request).build
+
+ event_options = EventOptionsBuilder(event_type = EventTypes::LOG_IN,
+ user_id = 'USER_ID', user_traits = UserTraits('USER_NAME', 'USER_EMAIL'),
+ context = context, properties = {prop1 => 'CUSTOM_PARAM_VALUE', prop2 => true, prop3 => 3}).build
+
+ securenative.track(event_options)
end
```
-## Verification events
-Once the SDK has been initialized, you can start sending new events with the `verify` function:
+## Verify events
+
+**Example**
+
```ruby
require 'securenative'
-require 'securenative/event_type'
-
-
-def reset_password
- res = SecureNative.verify(Event.new(
- event_type = EventTypes::PASSWORD_RESET,
- user: User.new("1", "Jon Snow", "jon@snow.com"),
- ip: "1.2.3.4",
- remote_ip: "5.6.7.8",
- user_agent: "Mozilla/5.0 (Linux; U; Android 4.4.2; zh-cn; GT-I9500 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko)Version/4.0 MQQBrowser/5.0 QQ-URL-Manager Mobile Safari/537.36",
- sn_cookie: "cookie"))
-
- if res['riskLevel'] == 'high'
- return 'Cannot change password'
- else res['riskLevel'] == 'medium'
- return 'MFA'
- end
+require 'securenative/enums/event_types'
+require 'securenative/event_options_builder'
+require 'securenative/models/user_traits'
+
+
+def track(request)
+ securenative = SecureNative.instance
+ context = SecureNative.context_builder.from_http_request(request).build
+
+ event_options = EventOptionsBuilder(event_type = EventTypes::LOG_IN,
+ user_id = 'USER_ID', user_traits = UserTraits('USER_NAME', 'USER_EMAIL'),
+ context = context, properties = {prop1 => 'CUSTOM_PARAM_VALUE', prop2 => true, prop3 => 3}).build
+
+ verify_result = securenative.verify(event_options)
+ verify_result.risk_level # Low, Medium, High
+ verify_result.score # Risk score: 0 -1 (0 - Very Low, 1 - Very High)
+ verify_result.triggers # ["TOR", "New IP", "New City"]
end
```
-## Using webhooks
-You can use the SDK to verify incoming webhooks from SecureNative, just call the `veriy_webhook` function which return a boolean which indicates if the webhook came from Secure Native servers.
+## Webhook signature verification
+
+Apply our filter to verify the request is from us, for example:
+
```ruby
require 'securenative'
-begin
- SecureNative.init('YOUR_API_KEY') # Should be called before any other call to securenative
-rescue SecureNativeSDKException => e
- # Do some error handling
-end
-def handle_some_code(headers, body)
- sig_header = headers["X-SecureNative"]
- if SecureNative.verify_webhook(sig_header, body)
- # Handle the webhook
- level = body['riskLevel']
- else
- # This request wasn't sent from Secure Native servers, you can dismiss/investigate it
- end
+def webhook_endpoint(request)
+ securenative = SecureNative.instance
+
+ # Checks if request is verified
+ is_verified = securenative.verify_request_payload(request)
end
-```
+ ```
\ No newline at end of file
diff --git a/Rakefile b/Rakefile
index 6a164d1..f912feb 100644
--- a/Rakefile
+++ b/Rakefile
@@ -1,2 +1,6 @@
require "bundler/gem_tasks"
-task :default => :spec
\ No newline at end of file
+task :default => :spec
+
+require 'rspec/core/rake_task'
+task :default => :spec
+RSpec::Core::RakeTask.new
\ No newline at end of file
diff --git a/VERSION b/VERSION
new file mode 100644
index 0000000..99a25fc
--- /dev/null
+++ b/VERSION
@@ -0,0 +1 @@
+0.1.16
\ No newline at end of file
diff --git a/lib/securenative.rb b/lib/securenative.rb
deleted file mode 100644
index ee6f2d3..0000000
--- a/lib/securenative.rb
+++ /dev/null
@@ -1,45 +0,0 @@
-require_relative 'securenative/sn_exception'
-require_relative 'securenative/secure_native_sdk'
-require 'logger'
-
-$securenative = nil
-$logger = Logger.new(STDOUT)
-$logger.level = Logger::INFO
-
-module SecureNative
- def self.init(api_key, options: SecureNativeOptions.new)
- if $securenative == nil
- $securenative = SecureNativeSDK.new(api_key, options: options)
- else
- $logger.info("This SDK was already initialized")
- raise StandardError.new("This SDK was already initialized")
- end
- end
-
- def self.track(event)
- sdk = _get_or_throw
- sdk.track(event)
- end
-
- def self.verify(event)
- sdk = _get_or_throw
- sdk.verify(event)
- end
-
- def self.verify_webhook(hmac_header, body)
- sdk = _get_or_throw
- sdk.verify_webhook(hmac_header = hmac_header, body = body)
- end
-
- def self.flush
- sdk = _get_or_throw
- sdk.flush
- end
-
- def self._get_or_throw
- if $securenative == nil
- raise SecureNativeSDKException.new
- end
- $securenative
- end
-end
\ No newline at end of file
diff --git a/lib/securenative/api_manager.rb b/lib/securenative/api_manager.rb
new file mode 100644
index 0000000..0d6e641
--- /dev/null
+++ b/lib/securenative/api_manager.rb
@@ -0,0 +1,30 @@
+require 'json'
+require_relative 'logger'
+
+class ApiManager
+ def initialize(event_manager, securenative_options)
+ @event_manager = event_manager
+ @options = securenative_options
+ end
+
+ def track(event_options)
+ Logger.debug('Track event call')
+ event = SDKEvent.new(event_options, @options)
+ @event_manager.send_async(event, ApiRoute::TRACK)
+ end
+
+ def verify(event_options)
+ Logger.debug('Verify event call')
+ event = SDKEvent.new(event_options, @options)
+
+ begin
+ res = JSON.parse(@event_manager.send_sync(event, ApiRoute::VERIFY, false))
+ return VerifyResult.new(res['riskLevel'], res['score'], res['triggers'])
+ rescue StandardError => e
+ Logger.debug('Failed to call verify; {}'.format(e))
+ end
+ return VerifyResult.new(RiskLevel::LOW, 0, nil) if @options.fail_over_strategy == FailOverStrategy::FAIL_OPEN
+
+ VerifyResult.new(RiskLevel::HIGH, 1, nil)
+ end
+end
\ No newline at end of file
diff --git a/lib/securenative/config.rb b/lib/securenative/config.rb
deleted file mode 100644
index 2a02d27..0000000
--- a/lib/securenative/config.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-module Config
- SDK_VERSION = '0.1.5'
- MAX_ALLOWED_PARAMS = 6
- API_URL_PROD = "https://api.securenative.com/collector/api/v1"
- API_URL_STG = "https://api.securenative-stg.com/collector/api/v1"
- TRACK_EVENT = "/track"
- VERIFY_EVENT = "/verify"
- FLOW_EVENT = "/flow"
- CIPHER_SIZE = 256
- AES_BLOCK_SIZE = 32
-end
\ No newline at end of file
diff --git a/lib/securenative/config/configuration_builder.rb b/lib/securenative/config/configuration_builder.rb
new file mode 100644
index 0000000..8cd563b
--- /dev/null
+++ b/lib/securenative/config/configuration_builder.rb
@@ -0,0 +1,26 @@
+class ConfigurationBuilder
+ attr_reader :api_key, :api_url, :interval, :max_events, :timeout, :auto_send, :disable, :log_level, :fail_over_strategy
+ attr_writer :api_key, :api_url, :interval, :max_events, :timeout, :auto_send, :disable, :log_level, :fail_over_strategy
+
+ def initialize(api_key = nil, api_url = 'https://api.securenative.com/collector/api/v1', interval = 1000,
+ max_events = 1000, timeout = 1500, auto_send = true, disable = false, log_level = 'FATAL',
+ fail_over_strategy = FailOverStrategy::FAIL_OPEN)
+ @api_key = api_key
+ @api_url = api_url
+ @interval = interval
+ @max_events = max_events
+ @timeout = timeout
+ @auto_send = auto_send
+ @disable = disable
+ @log_level = log_level
+ @fail_over_strategy = fail_over_strategy
+ end
+
+ def self.default_config_builder
+ ConfigurationBuilder()
+ end
+
+ def self.default_securenative_options
+ SecureNativeOptions()
+ end
+end
\ No newline at end of file
diff --git a/lib/securenative/config/configuration_manager.rb b/lib/securenative/config/configuration_manager.rb
new file mode 100644
index 0000000..cd28445
--- /dev/null
+++ b/lib/securenative/config/configuration_manager.rb
@@ -0,0 +1,53 @@
+require 'parseconfig'
+
+class ConfigurationManager
+ DEFAULT_CONFIG_FILE = 'securenative.cfg'.freeze
+ CUSTOM_CONFIG_FILE_ENV_NAME = 'SECURENATIVE_COMFIG_FILE'.freeze
+ @config = nil
+
+ def self.read_resource_file(resource_path)
+ @config = ParseConfig.new(resource_path)
+
+ properties = {}
+ @config.get_groups.each { |group|
+ group.each do |key, value|
+ properties[key.upcase] = value
+ end
+ }
+ properties
+ end
+
+ def self._get_resource_path(env_name)
+ Env.fetch(env_name, ENV[DEFAULT_CONFIG_FILE])
+ end
+
+ def self.config_builder
+ ConfigurationBuilder.default_config_builder
+ end
+
+ def self._get_env_or_default(properties, key, default)
+ return Env[key] if Env[key]
+ return properties[key] if properties[key]
+
+ default
+ end
+
+ def self.load_config
+ options = ConfigurationBuilder().default_securenative_options
+
+ resource_path = DEFAULT_CONFIG_FILE
+ resource_path = Env[CUSTOM_CONFIG_FILE_ENV_NAME] if Env[CUSTOM_CONFIG_FILE_ENV_NAME]
+
+ properties = read_resource_file(resource_path)
+
+ ConfigurationBuilder(_get_env_or_default(properties, 'SECURENATIVE_API_KEY', options.api_key),
+ _get_env_or_default(properties, 'SECURENATIVE_API_URL', options.api_url),
+ _get_env_or_default(properties, 'SECURENATIVE_INTERVAL', options.interval),
+ _get_env_or_default(properties, 'SECURENATIVE_MAX_EVENTS', options.max_events),
+ _get_env_or_default(properties, 'SECURENATIVE_TIMEOUT', options.timeout),
+ _get_env_or_default(properties, 'SECURENATIVE_AUTO_SEND', options.auto_send),
+ _get_env_or_default(properties, 'SECURENATIVE_DISABLE', options.disable),
+ _get_env_or_default(properties, 'SECURENATIVE_LOG_LEVEL', options.log_level),
+ _get_env_or_default(properties, 'SECURENATIVE_FAILOVER_STRATEGY', options.fail_over_strategy))
+ end
+end
\ No newline at end of file
diff --git a/lib/securenative/config/securenative_options.rb b/lib/securenative/config/securenative_options.rb
new file mode 100644
index 0000000..b801cb8
--- /dev/null
+++ b/lib/securenative/config/securenative_options.rb
@@ -0,0 +1,18 @@
+class SecureNativeOptions
+ attr_reader :api_key, :api_url, :interval, :max_events, :timeout, :auto_send, :disable, :log_level, :fail_over_strategy
+ attr_writer :api_key, :api_url, :interval, :max_events, :timeout, :auto_send, :disable, :log_level, :fail_over_strategy
+
+ def initialize(api_key = nil, api_url = "https://api.securenative.com/collector/api/v1", interval = 1000,
+ max_events = 1000, timeout = 1500, auto_send = true, disable = false, log_level = "FATAL",
+ fail_over_strategy = FailOverStrategy::FAIL_OPEN)
+ @api_key = api_key
+ @api_url = api_url
+ @interval = interval
+ @max_events = max_events
+ @timeout = timeout
+ @auto_send = auto_send
+ @disable = disable
+ @log_level = log_level
+ @fail_over_strategy = fail_over_strategy
+ end
+end
\ No newline at end of file
diff --git a/lib/securenative/context/context_builder.rb b/lib/securenative/context/context_builder.rb
new file mode 100644
index 0000000..297d42f
--- /dev/null
+++ b/lib/securenative/context/context_builder.rb
@@ -0,0 +1,59 @@
+class ContextBuilder
+ attr_reader :context
+
+ def initialize(client_token = nil, ip = nil, remote_ip = nil, headers = nil, url = nil, method = nil, body = nil)
+ @context = SecureNativeContext(client_token, ip, remote_ip, headers, url, method, body)
+ end
+
+ def client_token(client_token)
+ @context.client_token = client_token
+ end
+
+ def ip(ip)
+ @context.ip = ip
+ end
+
+ def remote_ip(remote_ip)
+ @context.remote_ip = remote_ip
+ end
+
+ def headers(headers)
+ @context.headers = headers
+ end
+
+ def url(url)
+ @context.url = url
+ end
+
+ def method(method)
+ @context.method = method
+ end
+
+ def body(body)
+ @context.body = body
+ end
+
+ def self.default_context_builder
+ ContextBuilder()
+ end
+
+ def self.from_http_request(request)
+ begin
+ client_token = request.cookies[RequestUtils.SECURENATIVE_COOKIE]
+ rescue StandardError
+ client_token = nil
+ end
+
+ begin
+ headers = request.headers
+ rescue StandardError
+ headers = nil
+ end
+
+ client_token = RequestUtils.get_secure_header_from_request(headers) if Utils.null_or_empty?(client_token)
+
+ ContextBuilder(url = request.url, method = request.method, header = headers, client_token = client_token,
+ client_ip = RequestUtils.get_client_ip_from_request(request),
+ remote_ip = RequestUtils.get_remote_ip_from_request(request), nil)
+ end
+end
\ No newline at end of file
diff --git a/lib/securenative/context/securenative_context.rb b/lib/securenative/context/securenative_context.rb
new file mode 100644
index 0000000..efb2a46
--- /dev/null
+++ b/lib/securenative/context/securenative_context.rb
@@ -0,0 +1,14 @@
+class SecureNativeContext
+ attr_reader :client_token, :ip, :remote_ip, :headers, :url, :method, :body
+ attr_writer :client_token, :ip, :remote_ip, :headers, :url, :method, :body
+
+ def initialize(client_token = nil, ip = nil, remote_ip = nil, headers = nil, url = nil, method = nil, body = nil)
+ @client_token = client_token
+ @ip = ip
+ @remote_ip = remote_ip
+ @headers = headers
+ @url = url
+ @method = method
+ @body = body
+ end
+end
\ No newline at end of file
diff --git a/lib/securenative/enums/api_route.rb b/lib/securenative/enums/api_route.rb
new file mode 100644
index 0000000..84c4a00
--- /dev/null
+++ b/lib/securenative/enums/api_route.rb
@@ -0,0 +1,4 @@
+module ApiRoute
+ TRACK = 'track'.freeze
+ VERIFY = 'verify'.freeze
+end
\ No newline at end of file
diff --git a/lib/securenative/enums/event_types.rb b/lib/securenative/enums/event_types.rb
new file mode 100644
index 0000000..76be140
--- /dev/null
+++ b/lib/securenative/enums/event_types.rb
@@ -0,0 +1,21 @@
+module EventTypes
+ LOG_IN = 'sn.user.login'.freeze
+ LOG_IN_CHALLENGE = 'sn.user.login.challenge'.freeze
+ LOG_IN_FAILURE = 'sn.user.login.failure'.freeze
+ LOG_OUT = 'sn.user.logout'.freeze
+ SIGN_UP = 'sn.user.signup'.freeze
+ AUTH_CHALLENGE = 'sn.user.auth.challenge'.freeze
+ AUTH_CHALLENGE_SUCCESS = 'sn.user.auth.challenge.success'.freeze
+ AUTH_CHALLENGE_FAILURE = 'sn.user.auth.challenge.failure'.freeze
+ TWO_FACTOR_DISABLE = 'sn.user.2fa.disable'.freeze
+ EMAIL_UPDATE = 'sn.user.email.update'.freeze
+ PASSWORD_REST = 'sn.user.password.reset'.freeze
+ PASSWORD_REST_SUCCESS = 'sn.user.password.reset.success'.freeze
+ PASSWORD_UPDATE = 'sn.user.password.update'.freeze
+ PASSWORD_REST_FAILURE = 'sn.user.password.reset.failure'.freeze
+ USER_INVITE = 'sn.user.invite'.freeze
+ ROLE_UPDATE = 'sn.user.role.update'.freeze
+ PROFILE_UPDATE = 'sn.user.profile.update'.freeze
+ PAGE_VIEW = 'sn.user.page.view'.freeze
+ VERIFY = 'sn.verify'.freeze
+end
\ No newline at end of file
diff --git a/lib/securenative/enums/failover_strategy.rb b/lib/securenative/enums/failover_strategy.rb
new file mode 100644
index 0000000..3ba4df6
--- /dev/null
+++ b/lib/securenative/enums/failover_strategy.rb
@@ -0,0 +1,4 @@
+module FailOverStrategy
+ FAIL_OPEN = 'fail-open'.freeze
+ FAIL_CLOSED = 'fail-closed'.freeze
+end
\ No newline at end of file
diff --git a/lib/securenative/enums/risk_level.rb b/lib/securenative/enums/risk_level.rb
new file mode 100644
index 0000000..01343c4
--- /dev/null
+++ b/lib/securenative/enums/risk_level.rb
@@ -0,0 +1,5 @@
+module RiskLevel
+ LOW = 'low'.freeze
+ MEDIUM = 'medium'.freeze
+ HIGH = 'high'.freeze
+end
\ No newline at end of file
diff --git a/lib/securenative/event_manager.rb b/lib/securenative/event_manager.rb
index fb7f4c8..cc6525b 100644
--- a/lib/securenative/event_manager.rb
+++ b/lib/securenative/event_manager.rb
@@ -1,88 +1,149 @@
-require_relative 'securenative_options'
-require_relative 'http_client'
-require_relative 'sn_exception'
-require 'json'
-require 'thread'
+require_relative 'logger'
class QueueItem
- def initialize(url, body)
+ attr_reader :url, :body, :retry
+ attr_writer :url, :body, :retry
+
+ def initialize(url, body, _retry)
@url = url
@body = body
+ @retry = _retry
end
-
- attr_reader :url
- attr_reader :body
end
class EventManager
- def initialize(api_key, options: SecureNativeOptions.new, http_client: HttpClient.new)
- if api_key == nil
- raise SecureNativeSDKException.new
+ def initialize(options = SecureNativeOptions(), http_client = nil)
+ if options.api_key.nil?
+ raise SecureNativeSDKException('API key cannot be None, please get your API key from SecureNative console.')
end
- @api_key = api_key
+ @http_client = if http_client.nil?
+ SecureNativeHttpClient(options)
+ else
+ http_client
+ end
+
+ @queue = []
+ @thread = Thread.new(run)
+ @thread.start
+
@options = options
- @http_client = http_client
- @queue = Queue.new
-
- if @options.auto_send
- interval_seconds = [(@options.interval / 1000).floor, 1].max
- Thread.new do
- loop do
- flush
- sleep(interval_seconds)
- end
- end
- end
+ @send_enabled = false
+ @attempt = 0
+ @coefficients = [1, 1, 2, 3, 5, 8, 13]
+ @thread = nil
+ @interval = options.interval
end
- def send_async(event, path)
- q_item = QueueItem.new(build_url(path), event)
- @queue.push(q_item)
- end
+ def send_async(event, resource_path)
+ if @options.disable
+ Logger.warning('SDK is disabled. no operation will be performed')
+ return
+ end
- def send_sync(event, path)
- @http_client.post(
- build_url(path),
- @api_key,
- event.to_hash.to_json
- )
+ item = QueueItem(resource_path, JSON.parse(EventManager.serialize(event)), false)
+ @queue.append(item)
end
def flush
- if is_queue_full
- i = @options.max_events - 1
- while i >= 0
- item = @queue.pop
- @http_client.post(
- build_url(item.url),
- @api_key,
- item.body.to_hash.to_json
- )
- end
- else
- q = Array.new(@queue.size) {@queue.pop}
- q.each do |item|
- @http_client.post(
- build_url(item.url),
- @api_key,
- item.body
- )
- @queue = Queue.new
- end
+ @queue.each do |item|
+ @http_client.post(item.url, item.body)
end
end
- private
+ def send_sync(event, resource_path, _retry)
+ if @options.disable
+ Logger.warning('SDK is disabled. no operation will be performed')
+ return
+ end
+
+ Logger.debug('Attempting to send event {}'.format(event))
+ res = @http_client.post(resource_path, JSON.parse(EventManager.serialize(event)))
- def build_url(path)
- @options.api_url + path
+ if res.status_code != 200
+ Logger.info('SecureNative failed to call endpoint {} with event {}. adding back to queue'.format(resource_path, event))
+ item = QueueItem(resource_path, JSON.parse(EventManager.serialize(event)), _retry)
+ @queue.append(item)
+ end
+
+ res
+ end
+
+ def run
+ loop do
+ next unless !@queue.empty? && @send_enabled
+
+ @queue.each do |item|
+ begin
+ res = @http_client.post(item.url, item.body)
+ if res.status_code == 401
+ item.retry = false
+ elsif res.status_code != 200
+ raise SecureNativeHttpException(res.status_code)
+ end
+ Logger.debug('Event successfully sent; {}'.format(item.body))
+ return res
+ rescue StandardError => e
+ Logger.error('Failed to send event; {}'.format(e))
+ if item.retry
+ @attempt = 0 if @coefficients.length == @attempt + 1
+
+ back_off = @coefficients[@attempt] * @options.interval
+ Logger.debug('Automatic back-off of {}'.format(back_off))
+ @send_enabled = false
+ sleep back_off
+ @send_enabled = true
+ end
+ end
+ end
+ sleep @interval / 1000
+ end
end
- private
+ def start_event_persist
+ Logger.debug('Starting automatic event persistence')
+ if @options.auto_send || @send_enabled
+ @send_enabled = true
+ else
+ Logger.debug('Automatic event persistence is disabled, you should persist events manually')
+ end
+ end
- def is_queue_full
- @queue.length > @options.max_events
+ def stop_event_persist
+ if @send_enabled
+ Logger.debug('Attempting to stop automatic event persistence')
+ begin
+ flush
+ @thread&.stop
+ Logger.debug('Stopped event persistence')
+ rescue StandardError => e
+ Logger.error('Could not stop event scheduler; {}'.format(e))
+ end
+ end
end
+ def self.serialize(obj)
+ {
+ rid: obj.rid,
+ eventType: obj.event_type,
+ userId: obj.user_id,
+ userTraits: {
+ name: obj.user_traits.name,
+ email: obj.user_traits.email,
+ createdAt: obj.user_traits.created_at
+ },
+ request: {
+ cid: obj.request.cid,
+ vid: obj.request.vid,
+ fp: obj.request.fp,
+ ip: obj.request.ip,
+ remoteIp: obj.request.remote_ip,
+ method: obj.request.method,
+ url: obj.request.url,
+ headers: obj.request.headers
+ },
+ timestamp: obj.timestamp,
+ properties: obj.properties
+ }
+ end
end
\ No newline at end of file
diff --git a/lib/securenative/event_options.rb b/lib/securenative/event_options.rb
deleted file mode 100644
index 0896e8d..0000000
--- a/lib/securenative/event_options.rb
+++ /dev/null
@@ -1,88 +0,0 @@
-require 'securerandom'
-
-class User
- def initialize(user_id: "", user_email: "", user_name: "")
- @user_id = user_id
- @user_email = user_email
- @user_name = user_name
- end
-
- attr_reader :user_id
- attr_reader :user_email
- attr_reader :user_name
-end
-
-class Event
- def initialize(event_type, user: User(), ip: "127.0.0.1", remote_ip: "127.0.0.1", user_agent: "unknown", sn_cookie: nil, params: nil)
- @event_type = event_type
- @user = user
- @ip = ip
- @remote_ip = remote_ip
- @user_agent = user_agent
- @cid = ""
- @fp = ""
-
- if params != nil
- if params.instance_of?(CustomParam)
- raise ArgumentError.new("custom params should be a list of CustomParams")
- else
- @params = params
- end
- end
- @params = params
-
- if sn_cookie
- @cid, @cid = Utils.parse_cookie(sn_cookie)
- end
- @vid = SecureRandom.uuid
- @ts = Time.now.getutc.to_i
- end
-
- def to_hash
- p = Array.new
- if @params
- @params.each do |param|
- p << {:key => param.key, :value => param.value}
- end
- end
-
- {
- :eventType => @event_type,
- :user => {
- :id => @user.user_id,
- :email => @user.user_email,
- :name => @user.user_name
- },
- :remoteIP => @remote_ip,
- :ip => @ip,
- :cid => @cid,
- :fp => @fp,
- :ts => @ts,
- :vid => @vid,
- :userAgent => @user_agent,
- :device => Hash.new,
- :params => p
- }
- end
-
- attr_reader :cid
- attr_reader :params
- attr_reader :user_agent
- attr_reader :user
- attr_reader :remote_ip
- attr_reader :event_type
- attr_reader :fp
- attr_reader :ip
- attr_reader :ts
- attr_reader :vid
-end
-
-class CustomParam
- def initialize(key, value)
- @key = key
- @value = value
- end
-
- attr_reader :key
- attr_reader :value
-end
diff --git a/lib/securenative/event_options_builder.rb b/lib/securenative/event_options_builder.rb
new file mode 100644
index 0000000..f931a21
--- /dev/null
+++ b/lib/securenative/event_options_builder.rb
@@ -0,0 +1,21 @@
+class EventOptionsBuilder
+ MAX_PROPERTIES_SIZE = 10
+
+ def initialize(event_type, user_id, user_traits, user_name, email, created_at, context, properties, timestamp)
+ @event_options = EventOptions(event_type)
+ @event_options.user_id = user_id
+ @event_options.user_traits = user_traits if user_traits
+ @event_options.user_traits = UserTraits(name, email, created_at) if user_name && email && created_at
+ @event_options.context = context
+ @event_options.properties = properties
+ @event_options.timestamp = timestamp
+ end
+
+ def build
+ if !@event_options.properties.nil? && @event_options.properties.length > MAX_PROPERTIES_SIZE
+ raise SecureNativeInvalidOptionsException('You can have only up to {} custom properties', MAX_PROPERTIES_SIZE)
+ end
+
+ @event_options
+ end
+end
\ No newline at end of file
diff --git a/lib/securenative/event_type.rb b/lib/securenative/event_type.rb
deleted file mode 100644
index 8749bdf..0000000
--- a/lib/securenative/event_type.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-module EventType
- LOG_IN = "sn.user.login"
- LOG_IN_CHALLENGE = "sn.user.login.challenge"
- LOG_IN_FAILURE = "sn.user.login.failure"
- LOG_OUT = "sn.user.logout"
- SIGN_UP = "sn.user.signup"
- AUTH_CHALLENGE = "sn.user.auth.challenge"
- AUTH_CHALLENGE_SUCCESS = "sn.user.auth.challenge.success"
- AUTH_CHALLENGE_FAILURE = "sn.user.auth.challenge.failure"
- TWO_FACTOR_DISABLE = "sn.user.2fa.disable"
- EMAIL_UPDATE = "sn.user.email.update"
- PASSWORD_RESET = "sn.user.password.reset"
- PASSWORD_RESET_SUCCESS = "sn.user.password.reset.success"
- PASSWORD_UPDATE = "sn.user.password.update"
- PASSWORD_RESET_FAILURE = "sn.user.password.reset.failure"
- USER_INVITE = "sn.user.invite"
- ROLE_UPDATE = "sn.user.role.update"
- PROFILE_UPDATE = "sn.user.profile.update"
- PAGE_VIEW = "sn.user.page.view"
- VERIFY = "sn.verify"
-end
\ No newline at end of file
diff --git a/lib/securenative/exceptions/securenative_config_exception.rb b/lib/securenative/exceptions/securenative_config_exception.rb
new file mode 100644
index 0000000..1c6bb67
--- /dev/null
+++ b/lib/securenative/exceptions/securenative_config_exception.rb
@@ -0,0 +1,2 @@
+class SecureNativeConfigException < StandardError
+end
\ No newline at end of file
diff --git a/lib/securenative/exceptions/securenative_http_exception.rb b/lib/securenative/exceptions/securenative_http_exception.rb
new file mode 100644
index 0000000..85314aa
--- /dev/null
+++ b/lib/securenative/exceptions/securenative_http_exception.rb
@@ -0,0 +1,2 @@
+class SecureNativeHttpException < StandardError
+end
\ No newline at end of file
diff --git a/lib/securenative/exceptions/securenative_invalid_options_exception.rb b/lib/securenative/exceptions/securenative_invalid_options_exception.rb
new file mode 100644
index 0000000..6b8be6d
--- /dev/null
+++ b/lib/securenative/exceptions/securenative_invalid_options_exception.rb
@@ -0,0 +1,2 @@
+class SecureNativeInvalidOptionsException < StandardError
+end
\ No newline at end of file
diff --git a/lib/securenative/exceptions/securenative_invalid_uri_exception.rb b/lib/securenative/exceptions/securenative_invalid_uri_exception.rb
new file mode 100644
index 0000000..68b722b
--- /dev/null
+++ b/lib/securenative/exceptions/securenative_invalid_uri_exception.rb
@@ -0,0 +1,2 @@
+class SecureNativeInvalidUriException < StandardError
+end
\ No newline at end of file
diff --git a/lib/securenative/exceptions/securenative_parse_exception.rb b/lib/securenative/exceptions/securenative_parse_exception.rb
new file mode 100644
index 0000000..ce65aee
--- /dev/null
+++ b/lib/securenative/exceptions/securenative_parse_exception.rb
@@ -0,0 +1,2 @@
+class SecureNativeParseException < StandardError
+end
\ No newline at end of file
diff --git a/lib/securenative/exceptions/securenative_sdk_Illegal_state_exception.rb b/lib/securenative/exceptions/securenative_sdk_Illegal_state_exception.rb
new file mode 100644
index 0000000..536ef7e
--- /dev/null
+++ b/lib/securenative/exceptions/securenative_sdk_Illegal_state_exception.rb
@@ -0,0 +1,2 @@
+class SecureNativeSDKIllegalStateException < StandardError
+end
\ No newline at end of file
diff --git a/lib/securenative/exceptions/securenative_sdk_exception.rb b/lib/securenative/exceptions/securenative_sdk_exception.rb
new file mode 100644
index 0000000..02412f5
--- /dev/null
+++ b/lib/securenative/exceptions/securenative_sdk_exception.rb
@@ -0,0 +1,2 @@
+class SecureNativeSDKException < StandardError
+end
\ No newline at end of file
diff --git a/lib/securenative/http/http_response.rb b/lib/securenative/http/http_response.rb
new file mode 100644
index 0000000..27541aa
--- /dev/null
+++ b/lib/securenative/http/http_response.rb
@@ -0,0 +1,10 @@
+class HttpResponse
+ attr_reader :ok, :status_code, :body
+ attr_writer :ok, :status_code, :body
+
+ def initialize(ok, status_code, body)
+ @ok = ok
+ @status_code = status_code
+ @body = body
+ end
+end
\ No newline at end of file
diff --git a/lib/securenative/http/securenative_http_client.rb b/lib/securenative/http/securenative_http_client.rb
new file mode 100644
index 0000000..a764b62
--- /dev/null
+++ b/lib/securenative/http/securenative_http_client.rb
@@ -0,0 +1,30 @@
+require 'httpclient'
+
+class SecureNativeHttpClient
+ AUTHORIZATION_HEADER = 'Authorization'.freeze
+ VERSION_HEADER = 'SN-Version'.freeze
+ USER_AGENT_HEADER = 'User-Agent'.freeze
+ USER_AGENT_HEADER_VALUE = 'SecureNative-python'.freeze
+ CONTENT_TYPE_HEADER = 'Content-Type'.freeze
+ CONTENT_TYPE_HEADER_VALUE = 'application/json'.freeze
+
+ def __init__(securenative_options)
+ @options = securenative_options
+ @client = HTTPClient.new
+ end
+
+ def _headers
+ {
+ CONTENT_TYPE_HEADER => CONTENT_TYPE_HEADER_VALUE,
+ USER_AGENT_HEADER => USER_AGENT_HEADER_VALUE,
+ VERSION_HEADER => VersionUtils.version,
+ AUTHORIZATION_HEADER => options.api_key
+ }
+ end
+
+ def post(path, body)
+ url = '{}/{}'.format(@options.api_url, path)
+ headers = _headers
+ @client.post(url, body, headers)
+ end
+end
\ No newline at end of file
diff --git a/lib/securenative/http_client.rb b/lib/securenative/http_client.rb
deleted file mode 100644
index bf28c92..0000000
--- a/lib/securenative/http_client.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-require 'httpclient'
-
-class HttpClient
- def initialize
- @client = HTTPClient.new
- end
-
- def headers(api_key)
- {
- "Content-Type" => 'application/json',
- "User-Agent" => 'SecureNative-ruby',
- "Sn-Version" => Config::SDK_VERSION,
- "Authorization" => api_key
- }
- end
-
- def post(url, api_key, body)
- @client.post(url, body, self.headers(api_key))
- end
-end
\ No newline at end of file
diff --git a/lib/securenative/logger.rb b/lib/securenative/logger.rb
new file mode 100644
index 0000000..96bc718
--- /dev/null
+++ b/lib/securenative/logger.rb
@@ -0,0 +1,42 @@
+require 'logger'
+
+class Logger
+ @logger = Logger.new(STDOUT)
+
+ def self.init_logger(level)
+ @logger.level = case level
+ when 'WARN'
+ Logger::WARN
+ when 'DEBUG'
+ Logger::DEBUG
+ when 'ERROR'
+ Logger::ERROR
+ when 'FATAL'
+ Logger::FATAL
+ when 'INFO'
+ Logger::INFO
+ else
+ Logger::FATAL
+ end
+
+ @logger.formatter = proc do |severity, datetime, progname, msg|
+ "[#{datetime}] #{severity} (#{progname}): #{msg}\n"
+ end
+ end
+
+ def self.info(msg)
+ @logger.info(msg)
+ end
+
+ def self.debug(msg)
+ @logger.debug(msg)
+ end
+
+ def self.warning(msg)
+ @logger.warning(msg)
+ end
+
+ def self.error(msg)
+ @logger.error(msg)
+ end
+end
\ No newline at end of file
diff --git a/lib/securenative/models/client_token.rb b/lib/securenative/models/client_token.rb
new file mode 100644
index 0000000..8f3c9dd
--- /dev/null
+++ b/lib/securenative/models/client_token.rb
@@ -0,0 +1,10 @@
+class ClientToken
+ attr_reader :cid, :vid, :fp
+ attr_writer :cid, :vid, :fp
+
+ def initialize(cid, vid, fp)
+ @cid = cid
+ @vid = vid
+ @fp = fp
+ end
+end
diff --git a/lib/securenative/models/device.rb b/lib/securenative/models/device.rb
new file mode 100644
index 0000000..5ce321d
--- /dev/null
+++ b/lib/securenative/models/device.rb
@@ -0,0 +1,8 @@
+class Device
+ attr_reader :device_id
+ attr_writer :device_id
+
+ def initialize(device_id)
+ @device_id = device_id
+ end
+end
\ No newline at end of file
diff --git a/lib/securenative/models/event_options.rb b/lib/securenative/models/event_options.rb
new file mode 100644
index 0000000..fb8569b
--- /dev/null
+++ b/lib/securenative/models/event_options.rb
@@ -0,0 +1,13 @@
+class EventOptions
+ attr_reader :event, :user_id, :user_traits, :context, :properties, :timestamp
+ attr_writer :event, :user_id, :user_traits, :context, :properties, :timestamp
+
+ def initialize(event, user_id = nil, user_traits = nil, context = nil, properties = nil, timestamp = nil)
+ @event = event
+ @user_id = user_id
+ @user_traits = user_traits
+ @context = context
+ @properties = properties
+ @timestamp = timestamp
+ end
+end
\ No newline at end of file
diff --git a/lib/securenative/models/request_context.rb b/lib/securenative/models/request_context.rb
new file mode 100644
index 0000000..8995750
--- /dev/null
+++ b/lib/securenative/models/request_context.rb
@@ -0,0 +1,15 @@
+class RequestContext
+ attr_reader :cid, :vid, :fp, :ip, :remote_ip, :headers, :url, :method
+ attr_writer :cid, :vid, :fp, :ip, :remote_ip, :headers, :url, :method
+
+ def initialize(cid = nil, vid = nil, fp = nil, ip = nil, remote_ip = nil, headers = nil, url = nil, method = nil)
+ @cid = cid
+ @vid = vid
+ @fp = fp
+ @ip = ip
+ @remote_ip = remote_ip
+ @headers = headers
+ @url = url
+ @method = method
+ end
+end
diff --git a/lib/securenative/models/request_options.rb b/lib/securenative/models/request_options.rb
new file mode 100644
index 0000000..fc1d47c
--- /dev/null
+++ b/lib/securenative/models/request_options.rb
@@ -0,0 +1,10 @@
+class RequestOptions
+ attr_reader :url, :body, :retry
+ attr_writer :url, :body, :retry
+
+ def initialize(url, body, _retry)
+ @url = url
+ @body = body
+ @retry = _retry
+ end
+end
\ No newline at end of file
diff --git a/lib/securenative/models/sdk_event.rb b/lib/securenative/models/sdk_event.rb
new file mode 100644
index 0000000..4195fae
--- /dev/null
+++ b/lib/securenative/models/sdk_event.rb
@@ -0,0 +1,25 @@
+class SDKEvent
+ attr_reader :context, :rid, :event_type, :user_id, :user_traits, :request, :timestamp, :properties
+ attr_writer :context, :rid, :event_type, :user_id, :user_traits, :request, :timestamp, :properties
+
+ def initialize(event_options, securenative_options)
+ @context = if !event_options.context.nil?
+ event_options.context
+ else
+ ContextBuilder.default_context_builder
+ end
+
+ client_token = EncryptionUtils.decrypt(@context.client_token, securenative_options.api_key)
+
+ @rid = SecureRandom.uuid.to_str
+ @event_type = event_options.event
+ @user_id = event_options.user_id
+ @user_traits = event_options.user_traits
+ @request = RequestContext(cid = client_token ? client_token.cid : '', vid = client_token ? client_token.vid : '',
+ fp = client_token ? client_token.fp : '', ip = @context.ip, remote_ip = @context.remote_ip,
+ method = @context.method, url = @context.url, headers = @context.headers)
+
+ @timestamp = DateUtils.to_timestamp(event_options.timestamp)
+ @properties = event_options.properties
+ end
+end
\ No newline at end of file
diff --git a/lib/securenative/models/user_traits.rb b/lib/securenative/models/user_traits.rb
new file mode 100644
index 0000000..7499e66
--- /dev/null
+++ b/lib/securenative/models/user_traits.rb
@@ -0,0 +1,10 @@
+class UserTraits
+ attr_reader :name, :email, :created_at
+ attr_writer :name, :email, :created_at
+
+ def initialize(name = nil, email = nil, created_at = nil)
+ @name = name
+ @email = email
+ @created_at = created_at
+ end
+end
\ No newline at end of file
diff --git a/lib/securenative/models/verify_result.rb b/lib/securenative/models/verify_result.rb
new file mode 100644
index 0000000..c5e74b0
--- /dev/null
+++ b/lib/securenative/models/verify_result.rb
@@ -0,0 +1,10 @@
+class VerifyResult
+ attr_reader :risk_level, :score, :triggers
+ attr_writer :risk_level, :score, :triggers
+
+ def initialize(risk_level = nil, score = nil, triggers = nil)
+ @risk_level = risk_level
+ @score = score
+ @triggers = triggers
+ end
+end
\ No newline at end of file
diff --git a/lib/securenative/secure_native_sdk.rb b/lib/securenative/secure_native_sdk.rb
deleted file mode 100644
index 48bb924..0000000
--- a/lib/securenative/secure_native_sdk.rb
+++ /dev/null
@@ -1,78 +0,0 @@
-require_relative 'event_manager'
-require_relative 'config'
-require_relative 'sn_exception'
-require_relative 'utils'
-require 'json'
-require 'logger'
-
-class SecureNativeSDK
- def initialize(api_key, options: SecureNativeOptions.new)
- if api_key == nil
- raise SecureNativeSDKException.new
- end
-
- if options.debug
- @logger = Logger.new(STDOUT)
- @logger.level = Logger::INFO
- @logger.info("sn logging was activated")
- end
-
- @api_key = api_key
- @options = options
- @event_manager = EventManager.new(@api_key, options: @options)
- end
-
- def api_key
- @api_key
- end
-
- def version
- Config::SDK_VERSION
- end
-
- def track(event)
- if @options.debug
- @logger.info("Track event was called")
- end
- validate_event(event)
- @event_manager.send_async(event, Config::TRACK_EVENT)
- end
-
- def verify(event)
- if @options.debug
- @logger.info("Verify event was called")
- end
- validate_event(event)
- res = @event_manager.send_sync(event, Config::VERIFY_EVENT)
- if res.status_code == 200
- return JSON.parse(res.body)
- end
- nil
- end
-
- def flow(event) # Note: For future purposes
- validate_event(event)
- @event_manager.send_async(event, Config::FLOW_EVENT)
- end
-
- def verify_webhook(hmac_header, body)
- if @options.debug
- @logger.info("Verify webhook was called")
- end
- Utils.verify_signature(@api_key, body, hmac_header)
- end
-
- def flush
- @event_manager.flush
- end
-
- private
-
- def validate_event(event)
- unless event.params.nil?
- if event.params.length > Config::MAX_ALLOWED_PARAMS
- event.params = event.params[0, Config::MAX_ALLOWED_PARAMS]
- end
- end
- end
-end
\ No newline at end of file
diff --git a/lib/securenative/securenative.iml b/lib/securenative/securenative.iml
new file mode 100644
index 0000000..c87f26a
--- /dev/null
+++ b/lib/securenative/securenative.iml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/lib/securenative/securenative.rb b/lib/securenative/securenative.rb
new file mode 100644
index 0000000..dd83e58
--- /dev/null
+++ b/lib/securenative/securenative.rb
@@ -0,0 +1,82 @@
+require_relative 'logger'
+require_relative 'utils/signature_utils'
+
+class SecureNative
+ attr_reader :options
+
+ def initialize(options)
+ @securenative = nil
+ raise SecureNativeSDKException('You must pass your SecureNative api key') if Utils.null_or_empty?(options.api_key)
+
+ @options = options
+ @event_manager = EventManager(@options)
+
+ @event_manager.start_event_persist unless @options.api_url.nil?
+
+ @api_manager = ApiManager.new(@event_manager, @options)
+ Logger.init_logger(@options.log_level)
+ end
+
+ def self.init_with_options(options)
+ if @securenative.nil?
+ @securenative = SecureNative.new(options)
+ @securenative
+ else
+ Logger.debug('This SDK was already initialized.')
+ raise SecureNativeSDKException('This SDK was already initialized.')
+ end
+ end
+
+ def self.init_with_api_key(api_key)
+ raise SecureNativeConfigException('You must pass your SecureNative api key') if Utils.null_or_empty?(api_key)
+
+ if @securenative.nil?
+ options = ConfigurationBuilder(api_key = api_key)
+ @securenative = SecureNative.new(options)
+ @securenative
+ else
+ Logger.debug('This SDK was already initialized.')
+ raise SecureNativeSDKException(u('This SDK was already initialized.'))
+ end
+ end
+
+ def self.init
+ options = ConfigurationManager.load_config
+ init_with_options(options)
+ end
+
+ def self.instance
+ raise SecureNativeSDKIllegalStateException() if @securenative.nil?
+
+ @securenative
+ end
+
+ def self.config_builder(api_key = nil, api_url = 'https://api.securenative.com/collector/api/v1', interval = 1000,
+ max_events = 1000, timeout = 1500, auto_send = true, disable = false, log_level = 'FATAL',
+ fail_over_strategy = FailOverStrategy::FAIL_OPEN)
+ ConfigurationBuilder(api_key, api_url, interval, max_events, timeout, auto_send, disable, log_level, fail_over_strategy)
+ end
+
+ def self.context_builder(client_token = nil, ip = nil, remote_ip = nil, headers = nil, url = nil, method = nil, body = nil)
+ ContextBuilder(client_token, ip, remote_ip, headers, url, method, body)
+ end
+
+ def track(event_options)
+ @api_manager.track(event_options)
+ end
+
+ def verify(event_options)
+ @api_manager.verify(event_options)
+ end
+
+ def self._flush
+ @securenative = nil
+ end
+
+ def verify_request_payload(request)
+ request_signature = request.header[SignatureUtils.SIGNATURE_HEADER]
+ body = request.body
+
+ SignatureUtils.valid_signature?(@options.api_key, body, request_signature)
+ end
+end
\ No newline at end of file
diff --git a/lib/securenative/securenative_options.rb b/lib/securenative/securenative_options.rb
deleted file mode 100644
index 7af850a..0000000
--- a/lib/securenative/securenative_options.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-require_relative 'config'
-
-class SecureNativeOptions
- def initialize(api_url: Config::API_URL_PROD, interval: 1000, max_events: 1000, timeout: 1500, auto_send: true, debug: false)
- @timeout = timeout
- @max_events = max_events
- @api_url = api_url
- @interval = interval
- @auto_send = auto_send
- @debug = debug
- end
-
- attr_reader :timeout
- attr_reader :max_events
- attr_reader :api_url
- attr_reader :interval
- attr_reader :auto_send
- attr_reader :debug
-end
\ No newline at end of file
diff --git a/lib/securenative/sn_exception.rb b/lib/securenative/sn_exception.rb
deleted file mode 100644
index bfa85e1..0000000
--- a/lib/securenative/sn_exception.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-class SecureNativeSDKException < StandardError
- def initialize(msg: "API key cannot be nil, please get your API key from SecureNative console")
- super
- end
-end
\ No newline at end of file
diff --git a/lib/securenative/utils.rb b/lib/securenative/utils.rb
deleted file mode 100644
index 229894b..0000000
--- a/lib/securenative/utils.rb
+++ /dev/null
@@ -1,74 +0,0 @@
-require_relative 'config'
-require "logger"
-require "base64"
-require "json"
-require 'openssl'
-
-
-module Utils
- def self.verify_signature(secret, text_body, header_signature)
- begin
- key = secret.encode('utf-8')
- body = text_body.encode('utf-8')
- calculated_signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha512'), key, body)
- calculated_signature.eql? header_signature
- rescue Exception
- return false
- end
- end
-
- def self.parse_cookie(cookie = nil)
- fp = ""
- cid = ""
- unless cookie
- return fp, cid
- end
-
- begin
- decoded_cookie = Base64.decode64(cookie)
- unless decoded_cookie
- decoded_cookie = "{}"
- end
- jsonified = JSON.generate(decoded_cookie)
- if jsonified["fp"]
- fp = jsonified["fp"]
- end
- if jsonified["cid"]
- cid = jsonified["cid"]
- end
- rescue Exception
- ensure
- return fp, cid
- end
- end
-
- def self.encrypt(plain_text, key)
- cipher = OpenSSL::Cipher::AES.new(Config::CIPHER_SIZE, :CBC).encrypt
- cipher.padding = 0
-
- if plain_text.size % Config::AES_BLOCK_SIZE != 0
- logger = Logger.new(STDOUT)
- logger.level = Logger::WARN
- logger.fatal("data not multiple of block length")
- return nil
- end
-
- key = Digest::SHA1.hexdigest key
- cipher.key = key.slice(0, Config::AES_BLOCK_SIZE)
- s = cipher.update(plain_text) + cipher.final
-
- s.unpack('H*')[0].upcase
- end
-
- def self.decrypt(encrypted, key)
- cipher = OpenSSL::Cipher::AES.new(Config::CIPHER_SIZE, :CBC).decrypt
- cipher.padding = 0
-
- key = Digest::SHA1.hexdigest key
- cipher.key = key.slice(0, Config::AES_BLOCK_SIZE)
- s = [encrypted].pack("H*").unpack("C*").pack("c*")
-
- rv = cipher.update(s) + cipher.final
- return rv.strip
- end
-end
\ No newline at end of file
diff --git a/lib/securenative/utils/date_utils.rb b/lib/securenative/utils/date_utils.rb
new file mode 100644
index 0000000..cd9ac5a
--- /dev/null
+++ b/lib/securenative/utils/date_utils.rb
@@ -0,0 +1,7 @@
+class DateUtils
+ def self.to_timestamp(date)
+ return Time.now.strftime('%Y-%m-%dT%H:%M:%S.%L%Z') if date.nil?
+
+ Time.strptime(date, '%Y-%m-%dT%H:%M:%S.%L%Z')
+ end
+end
\ No newline at end of file
diff --git a/lib/securenative/utils/encryption_utils.rb b/lib/securenative/utils/encryption_utils.rb
new file mode 100644
index 0000000..67c8ef9
--- /dev/null
+++ b/lib/securenative/utils/encryption_utils.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require 'openssl'
+
+class EncryptionUtils
+ BLOCK_SIZE = 16
+ KEY_SIZE = 32
+
+ def self.encrypt(text, cipher_key)
+ cipher = OpenSSL::Cipher::AES.new(KEY_SIZE, :CBC).encrypt
+ cipher.padding = 0
+
+ if text.size % BLOCK_SIZE != 0
+ logger = Logger.new(STDOUT)
+ logger.level = Logger::WARN
+ logger.fatal('data not multiple of block length')
+ return nil
+ end
+
+ cipher_key = Digest::SHA1.hexdigest cipher_key
+ cipher.key = cipher_key.slice(0, BLOCK_SIZE)
+ s = cipher.update(text) + cipher.final
+
+ s.unpack('H*')[0].upcase
+ end
+
+ def self.decrypt(encrypted, cipher_key)
+ cipher = OpenSSL::Cipher::AES.new(KEY_SIZE, :CBC).decrypt
+ cipher.padding = 0
+
+ cipher_key = Digest::SHA1.hexdigest cipher_key
+ cipher.key = cipher_key.slice(0, BLOCK_SIZE)
+ s = [encrypted].pack('H*').unpack('C*').pack('c*')
+
+ rv = cipher.update(s) + cipher.final
+ rv.strip
+ end
+end
\ No newline at end of file
diff --git a/lib/securenative/utils/ip_utils.rb b/lib/securenative/utils/ip_utils.rb
new file mode 100644
index 0000000..91b892c
--- /dev/null
+++ b/lib/securenative/utils/ip_utils.rb
@@ -0,0 +1,22 @@
+class IpUtils
+ 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
+ VALID_IPV6_PATTERN = '([0-9a-f]{1,4}:){7}([0-9a-f]){1,4}'.freeze
+
+ def self.ip_address?(ip_address)
+ return true if IpUtils.VALID_IPV4_PATTERN.match(ip_address)
+ return true if IpUtils.VALID_IPV6_PATTERN.match(ip_address)
+
+ false
+ end
+
+ def self.valid_public_ip?(ip_address)
+ ip = IPAddr.new(ip_address)
+ return false if ip.loopback? || ip.private? || ip.link_local? || ip.untrusted? || ip.tainted?
+
+ true
+ end
+
+ def self.loop_back?(ip_address)
+ IPAddr.new(ip_address).loopback?
+ end
+end
\ No newline at end of file
diff --git a/lib/securenative/utils/request_utils.rb b/lib/securenative/utils/request_utils.rb
new file mode 100644
index 0000000..fc9b0ac
--- /dev/null
+++ b/lib/securenative/utils/request_utils.rb
@@ -0,0 +1,21 @@
+class RequestUtils
+ SECURENATIVE_COOKIE = '_sn'.freeze
+ SECURENATIVE_HEADER = 'x-securenative'.freeze
+
+ def self.get_secure_header_from_request(headers)
+ return headers[RequestUtils.SECURENATIVE_HEADER] unless headers.nil?
+
+ []
+ end
+
+ def self.get_client_ip_from_request(request)
+ x_forwarded_for = request.env['HTTP_X_FORWARDED_FOR']
+ return x_forwarded_for unless x_forwarded_for.nil?
+
+ request.env['REMOTE_ADDR']
+ end
+
+ def self.get_remote_ip_from_request(request)
+ request.remote_ip
+ end
+end
\ No newline at end of file
diff --git a/lib/securenative/utils/signature_utils.rb b/lib/securenative/utils/signature_utils.rb
new file mode 100644
index 0000000..9482563
--- /dev/null
+++ b/lib/securenative/utils/signature_utils.rb
@@ -0,0 +1,14 @@
+require 'openssl'
+
+class SignatureUtils
+ SIGNATURE_HEADER = 'x-securenative'.freeze
+
+ def self.valid_signature?(api_key, payload, header_signature)
+ key = api_key.encode('utf-8')
+ body = payload.encode('utf-8')
+ calculated_signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha512'), key, body)
+ calculated_signature.eql? header_signature
+ rescue StandardError
+ false
+ end
+end
\ No newline at end of file
diff --git a/lib/securenative/utils/utils.rb b/lib/securenative/utils/utils.rb
new file mode 100644
index 0000000..57641ad
--- /dev/null
+++ b/lib/securenative/utils/utils.rb
@@ -0,0 +1,9 @@
+class Utils
+ def self.null_or_empty?(string)
+ return true if !string.nil? && !string.empty?
+
+ return true unless string.nil?
+
+ false
+ end
+end
\ No newline at end of file
diff --git a/lib/securenative/utils/version_utils.rb b/lib/securenative/utils/version_utils.rb
new file mode 100644
index 0000000..d46e495
--- /dev/null
+++ b/lib/securenative/utils/version_utils.rb
@@ -0,0 +1,10 @@
+class VersionUtils
+ def self.version
+ path = 'VERSION'
+ file = File.open(path)
+ version = file.read
+ file.close
+
+ version
+ end
+end
\ No newline at end of file
diff --git a/securenative.gemspec b/securenative.gemspec
index 769e8ad..0e2cc88 100644
--- a/securenative.gemspec
+++ b/securenative.gemspec
@@ -1,14 +1,14 @@
lib = File.expand_path("lib", __dir__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
-require "securenative/config"
+require_relative "lib/securenative/utils/version_utils"
Gem::Specification.new do |spec|
spec.name = "securenative"
- spec.version = Config::SDK_VERSION
+ spec.version = VersionUtils.version
spec.authors = ["SecureNative"]
spec.email = ["support@securenative.com"]
- spec.summary = %q{SecureNative SDK for ruby}
+ spec.summary = %q{SecureNative SDK for Ruby}
spec.homepage = "https://www.securenative.com"
spec.license = "MIT"
@@ -24,5 +24,5 @@ Gem::Specification.new do |spec|
spec.require_paths = ["lib"]
spec.add_development_dependency "bundler", "~> 2.0"
- spec.add_development_dependency "rake", "~> 10.0"
+ spec.add_development_dependency "rake", "~> 12.3.3"
end
diff --git a/spec/spec_api_manager.rb b/spec/spec_api_manager.rb
new file mode 100644
index 0000000..74fb759
--- /dev/null
+++ b/spec/spec_api_manager.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+
+require 'webmock/rspec'
+
+describe ApiManager do
+ let(:context) do
+ ContextBuilder(ip = '127.0.0.1', client_token = 'SECURED_CLIENT_TOKEN', headers = {'user-agent' => 'Mozilla/5.0 (iPad; U; CPU OS 3_2_1 like Mac OS X; en-us)' \
+ 'AppleWebKit/531.21.10 (KHTML, like Gecko) Mobile/7B405'})
+ end
+ let(:event_options) do
+ EventOptionsBuilder(event_type = EventTypes.LOG_IN, user_id = 'USER_ID',
+ user_traits = UserTraits('USER_NAME', 'USER_EMAIL'),
+ properties = {prop1: 'CUSTOM_PARAM_VALUE', prop2: true, prop3: 3}).build
+ end
+
+ it 'tracks an event' do
+ options = ConfigurationBuilder(api_key = 'YOUR_API_KEY', auto_send = true, interval = 10, api_url = 'https://api.securenative-stg.com/collector/api/v1')
+
+ expected = '{"eventType":"sn.user.login","userId":"USER_ID","userTraits":{' \
+ '"name":"USER_NAME","email":"USER_EMAIL","createdAt":null},"request":{' \
+ '"cid":null,"vid":null,"fp":null,"ip":"127.0.0.1","remoteIp":null,"headers":{' \
+ '"user-agent":"Mozilla/5.0 (iPad; U; CPU OS 3_2_1 like Mac OS X; en-us) ' \
+ 'AppleWebKit/531.21.10 (KHTML, like Gecko) Mobile/7B405"},"url":null,"method":null},' \
+ '"properties":{"prop2":true,"prop1":"CUSTOM_PARAM_VALUE","prop3":3}}'
+
+ stub_request(:post, 'https://api.securenative-stg.com/collector/api/v1/track')
+ .with(body: JSON.parse(expected)).to_return(status: 200)
+ event_manager = EventManager.new(options)
+ event_manager.start_event_persist
+ api_manager = ApiManager.new(event_manager, options)
+
+ begin
+ api_manager.track(:event_options)
+ ensure
+ event_manager.stop_event_persist
+ end
+ end
+
+ it 'uses invalid options' do
+ options = ConfigurationBuilder(api_key = 'YOUR_API_KEY', auto_send = true, interval = 10, api_url = 'https://api.securenative-stg.com/collector/api/v1')
+
+ properties = {}
+ (0..12).each do |i|
+ properties[i] = i
+ end
+
+ stub_request(:post, 'https://api.securenative-stg.com/collector/api/v1/track').to_return(status: 200)
+ event_manager = EventManager.new(options)
+ event_manager.start_event_persist
+ api_manager = ApiManager.new(event_manager, options)
+
+ begin
+ expect { api_manager.track(EventOptionsBuilder(event_type = EventTypes.LOG_IN, properties = properties).build) }
+ .to raise_error(SecureNativeInvalidOptionsException)
+ ensure
+ event_manager.stop_event_persist
+ end
+ end
+
+ it 'verifies an event' do
+ options = ConfigurationBuilder(api_key = 'YOUR_API_KEY', api_url = 'https://api.securenative-stg.com/collector/api/v1')
+
+ stub_request(:post, 'https://api.securenative-stg.com/collector/api/v1/track')
+ .with(body: {riskLevel: 'medium', score: 0.32, triggers: ['New IP', 'New City']}).to_return(status: 200)
+ verify_result = VerifyResult.new(RiskLevel.LOW, 0, nil)
+
+ event_manager = EventManager.new(options)
+ event_manager.start_event_persist
+ api_manager = ApiManager.new(event_manager, options)
+
+ result = api_manager.verify(:event_options)
+
+ expect(result).not_to be_nil
+ expect(result.risk_level).to eq(verify_result.risk_level)
+ expect(result.score).to eq(verify_result.score)
+ expect(result.triggers).to eq(verify_result.triggers)
+ end
+end
\ No newline at end of file
diff --git a/spec/spec_context_builder.rb b/spec/spec_context_builder.rb
new file mode 100644
index 0000000..9c47f00
--- /dev/null
+++ b/spec/spec_context_builder.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+require 'webmock/rspec'
+
+describe ContextBuilder do
+ it 'creates context from request' do
+ stub_request(:any, 'www.example.com')
+ .to_return(body: nil, status: 200,
+ headers: { 'x-securenative': '71532c1fad2c7f56118f7969e401f3cf080239140d208e7934e6a530818c37e544a0c2330a487bcc6fe4f662a57f265a3ed9f37871e80529128a5e4f2ca02db0fb975ded401398f698f19bb0cafd68a239c6caff99f6f105286ab695eaf3477365bdef524f5d70d9be1d1d474506b433aed05d7ed9a435eeca357de57817b37c638b6bb417ffb101eaf856987615a77a' },
+ remote_ip: '', uri: 'www.securenative.com', method: 'Post', ip: '51.68.201.122',
+ client_token: '71532c1fad2c7f56118f7969e401f3cf080239140d208e7934e6a530818c37e544a0c2330a487bcc6fe4f662a57f265a3ed9f37871e80529128a5e4f2ca02db0fb975ded401398f698f19bb0cafd68a239c6caff99f6f105286ab695eaf3477365bdef524f5d70d9be1d1d474506b433aed05d7ed9a435eeca357de57817b37c638b6bb417ffb101eaf856987615a77a')
+
+ request = Net::HTTP.get('www.example.com', '/')
+ context = ContextBuilder.from_http_request(request)
+
+ expect(context.client_token).to eq('71532c1fad2c7f56118f7969e401f3cf080239140d208e7934e6a530818c37e544a0c2330a487bcc6fe4f662a57f265a3ed9f37871e80529128a5e4f2ca02db0fb975ded401398f698f19bb0cafd68a239c6caff99f6f105286ab695eaf3477365bdef524f5d70d9be1d1d474506b433aed05d7ed9a435eeca357de57817b37c638b6bb417ffb101eaf856987615a77a')
+ expect(context.ip).to eq('51.68.201.122')
+ expect(context.method).to eq('Post')
+ expect(context.uri).to eq('www.securenative.com')
+ expect(context.remote_ip).to eq('')
+ expect(context.headers).to eq({ 'x-securenative': '71532c1fad2c7f56118f7969e401f3cf080239140d208e7934e6a530818c37e544a0c2330a487bcc6fe4f662a57f265a3ed9f37871e80529128a5e4f2ca02db0fb975ded401398f698f19bb0cafd68a239c6caff99f6f105286ab695eaf3477365bdef524f5d70d9be1d1d474506b433aed05d7ed9a435eeca357de57817b37c638b6bb417ffb101eaf856987615a77a' })
+ expect(context.body).to be_nil
+ end
+
+ it 'creates context from request with cookie' do
+ stub_request(:any, 'www.example.com')
+ .to_return(body: nil, status: 200,
+ cookies: { '_sn': '71532c1fad2c7f56118f7969e401f3cf080239140d208e7934e6a530818c37e544a0c2330a487bcc6fe4f662a57f265a3ed9f37871e80529128a5e4f2ca02db0fb975ded401398f698f19bb0cafd68a239c6caff99f6f105286ab695eaf3477365bdef524f5d70d9be1d1d474506b433aed05d7ed9a435eeca357de57817b37c638b6bb417ffb101eaf856987615a77a' },
+ remote_ip: '', uri: 'www.securenative.com', method: 'Post', ip: '51.68.201.122',
+ client_token: '71532c1fad2c7f56118f7969e401f3cf080239140d208e7934e6a530818c37e544a0c2330a487bcc6fe4f662a57f265a3ed9f37871e80529128a5e4f2ca02db0fb975ded401398f698f19bb0cafd68a239c6caff99f6f105286ab695eaf3477365bdef524f5d70d9be1d1d474506b433aed05d7ed9a435eeca357de57817b37c638b6bb417ffb101eaf856987615a77a')
+
+ request = Net::HTTP.get('www.example.com', '/')
+ context = ContextBuilder.from_http_request(request)
+
+ expect(context.client_token).to eq('71532c1fad2c7f56118f7969e401f3cf080239140d208e7934e6a530818c37e544a0c2330a487bcc6fe4f662a57f265a3ed9f37871e80529128a5e4f2ca02db0fb975ded401398f698f19bb0cafd68a239c6caff99f6f105286ab695eaf3477365bdef524f5d70d9be1d1d474506b433aed05d7ed9a435eeca357de57817b37c638b6bb417ffb101eaf856987615a77a')
+ expect(context.ip).to eq('51.68.201.122')
+ expect(context.method).to eq('Post')
+ expect(context.uri).to eq('www.securenative.com')
+ expect(context.remote_ip).to eq('')
+ expect(context.body).to be_nil
+ end
+
+ it 'creates default context builder' do
+ context = ContextBuilder.default_context_builder.build
+
+ expect(context.client_token).not_to be_nil
+ expect(context.ip).not_to be_nil
+ expect(context.method).not_to be_nil
+ expect(context.url).not_to be_nil
+ expect(context.remote_ip).not_to be_nil
+ expect(context.headers).not_to be_nil
+ expect(context.body).not_to be_nil
+ end
+
+ it 'creates custom context with context builder' do
+ context = ContextBuilder(url = '/some-url', client_token = 'SECRET_TOKEN', ip = '10.0.0.0', body = '{ "name": "YOUR_NAME" }', method = 'Get', remote_ip = '10.0.0.1', headers = { header1: 'value1' })
+
+ expect(context.url).to eq('/some-url')
+ expect(context.client_token).to eq('SECRET_TOKEN')
+ expect(context.ip).to eq('10.0.0.0')
+ expect(context.body).to eq('{ "name": "YOUR_NAME" }')
+ expect(context.method).to eq('Get')
+ expect(context.remote_ip).to eq('10.0.0.0')
+ expect(context.headers).to eq({ header1: 'value1' })
+ end
+end
\ No newline at end of file
diff --git a/spec/spec_date_utils.rb b/spec/spec_date_utils.rb
new file mode 100644
index 0000000..5765b09
--- /dev/null
+++ b/spec/spec_date_utils.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+describe DateUtils do
+ it 'converts to timestamp' do
+ iso_8601_date = '2020-05-20T15:07:13Z'
+ result = DateUtils.to_timestamp(iso_8601_date)
+
+ expect(result).to eq(Time.now.strftime('%Y-%m-%dT%H:%M:%S.%L%Z'))
+ end
+end
\ No newline at end of file
diff --git a/spec/spec_encryption_utils.rb b/spec/spec_encryption_utils.rb
new file mode 100644
index 0000000..469d7c6
--- /dev/null
+++ b/spec/spec_encryption_utils.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+describe EncryptionUtils do
+ let(:SECRET_KEY) { 'B00C42DAD33EAC6F6572DA756EA4915349C0A4F6' }
+ let(:PAYLOAD) { '{"cid":"198a41ff-a10f-4cda-a2f3-a9ca80c0703b","vi":"148a42ff-b40f-4cda-a2f3-a8ca80c0703b","fp":"6d8cabd95987f8318b1fe01593d5c2a5.24700f9f1986800ab4fcc880530dd0ed"}' }
+ let(:CID) { '198a41ff-a10f-4cda-a2f3-a9ca80c0703b' }
+ let(:FP) { '6d8cabd95987f8318b1fe01593d5c2a5.24700f9f1986800ab4fcc880530dd0ed' }
+
+ it 'decrypts' do
+ result = EncryptionUtils.encrypt(:PAYLOAD, :SECRET_KEY)
+
+ expect(result).not_to be_nil
+ expect(result.length).to eq(:PAYLOAD.length)
+ end
+
+ it 'encrypts' do
+ encrypted_payload = '5208ae703cc2fa0851347f55d3b76d3fd6035ee081d71a401e8bc92ebdc25d42440f62310bda60628537744ac03f200d78da9e61f1019ce02087b7ce6c976e7b2d8ad6aa978c532cea8f3e744cc6a5cafedc4ae6cd1b08a4ef75d6e37aa3c0c76954d16d57750be2980c2c91ac7ef0bbd0722abd59bf6be22493ea9b9759c3ff4d17f17ab670b0b6fc320e6de982313f1c4e74c0897f9f5a32d58e3e53050ae8fdbebba9009d0d1250fe34dcde1ebb42acbc22834a02f53889076140f0eb8db1'
+ result = EncryptionUtils.decrypt(encrypted_payload, :SECRET_KEY)
+
+ expect(result.cid).to eq(:CID)
+ expect(result.fp).to eq(:FP)
+ end
+end
\ No newline at end of file
diff --git a/spec/spec_event_manager.rb b/spec/spec_event_manager.rb
new file mode 100644
index 0000000..9111381
--- /dev/null
+++ b/spec/spec_event_manager.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require 'webmock/rspec'
+
+class SampleEvent
+ attr_reader :event_type, :timestamp, :rid, :user_id, :user_traits, :request, :properties
+
+ def initialize
+ @event_type = 'custom-event'
+ @timestamp = Time.now.strftime('%Y-%m-%dT%H:%M:%S.%L%Z')
+ @rid = '432532'
+ @user_id = '1'
+ @user_traits = UserTraits('some user', 'email@securenative.com')
+ @request = RequestContext()
+ @properties = []
+ end
+end
+
+describe EventManager do
+ let(:event) { SampleEvent() }
+
+ it 'successfully sends sync event with status code 200' do
+ options = ConfigurationBuilder(api_key = 'YOUR_API_KEY', api_url = 'https://api.securenative-stg.com/collector/api/v1')
+
+ res_body = '{"data": true}'
+ stub_request(:post, 'https://api.securenative-stg.com/collector/api/v1/some-path/to-api')
+ .with(body: JSON.parse(res_body)).to_return(status: 200)
+ event_manager = EventManager.new(options)
+ data = event_manager.send_sync(:event, 'some-path/to-api', false)
+
+ expect(res_body).to eq(data.text)
+ end
+
+ it 'fails when send sync event status code is 401' do
+ options = ConfigurationBuilder(api_key = 'YOUR_API_KEY', api_url = 'https://api.securenative-stg.com/collector/api/v1')
+
+ stub_request(:post, 'https://api.securenative-stg.com/collector/api/v1/some-path/to-api').to_return(status: 401)
+ event_manager = EventManager.new(options)
+ res = event_manager.send_sync(:event, 'some-path/to-api', false)
+
+ expect(res.status_code).to eq(401)
+ end
+
+ it 'fails when send sync event status code is 500' do
+ options = ConfigurationBuilder(api_key = 'YOUR_API_KEY', api_url = 'https://api.securenative-stg.com/collector/api/v1')
+
+ stub_request(:post, 'https://api.securenative-stg.com/collector/api/v1/some-path/to-api').to_return(status: 500)
+ event_manager = EventManager.new(options)
+ res = event_manager.send_sync(:event, 'some-path/to-api', false)
+
+ expect(res.status_code).to eq(500)
+ end
+end
\ No newline at end of file
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
new file mode 100644
index 0000000..baa347b
--- /dev/null
+++ b/spec/spec_helper.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+require 'bundler/setup'
+require 'simplecov'
+require 'codecov'
+
+SimpleCov.start
+SimpleCov.formatter = SimpleCov::Formatter::Codecov
+
+RSpec.configure do |config|
+ # Enable flags like --only-failures and --next-failure
+ config.example_status_persistence_file_path = '.rspec_status'
+
+ # Disable RSpec exposing methods globally on `Module` and `main`
+ config.disable_monkey_patching!
+
+ config.expect_with :rspec do |c|
+ c.syntax = :expect
+ end
+end
\ No newline at end of file
diff --git a/spec/spec_ip_utils.rb b/spec/spec_ip_utils.rb
new file mode 100644
index 0000000..100d83b
--- /dev/null
+++ b/spec/spec_ip_utils.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+describe IpUtils do
+ it 'checks if ip address valid ipv4' do
+ valid_ipv4 = '172.16.254.1'
+ expect(IpUtils.ip_address?(valid_ipv4)).to be_truthy
+ end
+
+ it 'checks if ip address valid ipv6' do
+ valid_ipv6 = '2001:db8:1234:0000:0000:0000:0000:0000'
+ expect(IpUtils.ip_address?(valid_ipv6)).to be_truthy
+ end
+
+ it 'checks if ip address invalid ipv4' do
+ invalid_ipv4 = '172.16.2541'
+ expect(IpUtils.ip_address?(invalid_ipv4)).to be_falsey
+ end
+
+ it 'checks if ip address invalid ipv6' do
+ invalid_ipv6 = '2001:db8:1234:0000'
+ expect(IpUtils.ip_address?(invalid_ipv6)).to be_falsey
+ end
+
+ it 'checks if valid public ip' do
+ ip = '64.71.222.37'
+ expect(IpUtils.valid_public_ip?(ip)).to be_truthy
+ end
+
+ it 'checks if not valid public ip' do
+ ip = '10.0.0.0'
+ expect(IpUtils.valid_public_ip?(ip)).to be_falsey
+ end
+
+ it 'checks if valid loopback ip' do
+ ip = '127.0.0.1'
+ expect(IpUtils.loop_back?(ip)).to be_truthy
+ end
+end
\ No newline at end of file
diff --git a/spec/spec_securenative.rb b/spec/spec_securenative.rb
new file mode 100644
index 0000000..7b85bc0
--- /dev/null
+++ b/spec/spec_securenative.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+describe SecureNative do
+ it 'gets sdk instance without init throws' do
+ expect { SecureNative.instance }.to raise_error(SecureNativeSDKIllegalStateException)
+ end
+
+ it 'inits sdk without api key and throws' do
+ expect { SecureNative.init_with_options(ConfigurationManager.config_builder) }.to raise_error(SecureNativeSDKException)
+ end
+
+ it 'inits sdk with empty api key and throws' do
+ expect { SecureNative.init_with_api_key('') }.to raise_error(SecureNativeSDKException)
+ end
+
+ it 'inits sdk with api key and defaults' do
+ SecureNative._flush
+ api_key = 'API_KEY'
+ securenative = SecureNative.init_with_api_key(api_key)
+ options = securenative.options
+
+ expect(options.api_key).to eq(api_key)
+ expect(options.api_url).to eq('https://api.securenative.com/collector/api/v1')
+ expect(options.interval).to eq(1000)
+ expect(options.timeout).to eq(1500)
+ expect(options.max_events).to eq(100)
+ expect(options.auto_send).to eq(true)
+ expect(options.disable).to eq(false)
+ expect(options.log_level).to eq('FATAL')
+ expect(options.fail_over_strategy).to eq(FailOverStrategy::FAIL_OPEN)
+ end
+
+ it 'inits sdk twice and throws' do
+ expect { SecureNative.init_with_api_key('API_KEY') }.to raise_error(SecureNativeSDKException)
+ end
+
+ it 'inits sdk with api key and gets instance' do
+ SecureNative._flush
+ api_key = 'API_KEY'
+ securenative = SecureNative.init_with_api_key(api_key)
+
+ expect(securenative).to eq(SecureNative.instance)
+ end
+
+ it 'inits sdk with builder' do
+ SecureNative._flush
+ securenative = SecureNative.init_with_options(SecureNative.config_builder(api_key = 'API_KEY', max_events = 10, log_level = 'ERROR'))
+ options = securenative.options
+
+ expect(options.api_key).to eq('API_KEY')
+ expect(options.max_events).to eq(10)
+ expect(options.log_level).to eq('ERROR')
+ end
+end
\ No newline at end of file
diff --git a/spec/spec_securenative_http_client.rb b/spec/spec_securenative_http_client.rb
new file mode 100644
index 0000000..cc6e2ad
--- /dev/null
+++ b/spec/spec_securenative_http_client.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+describe SecureNativeHttpClient do
+ it 'makes a simple post call' do
+ options = ConfigurationBuilder(api_key = 'YOUR_API_KEY', api_url = 'https://api.securenative-stg.com/collector/api/v1')
+
+ stub_request(:post, 'https://api.securenative-stg.com/collector/api/v1/track')
+ .with(body: { event: 'SOME_EVENT_NAME' }).to_return(status: 200)
+ client = SecureNativeHttpClient(options)
+ payload = '{"event": "SOME_EVENT_NAME"}'
+
+ res = client.post('track', payload)
+
+ expect(res.ok).to eq(true)
+ expect(res.status_code).to eq(200)
+ expect(res.text).to eq(payload)
+ end
+end
\ No newline at end of file
diff --git a/spec/spec_signature_utils.rb b/spec/spec_signature_utils.rb
new file mode 100644
index 0000000..9d54d6d
--- /dev/null
+++ b/spec/spec_signature_utils.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+describe SignatureUtils do
+ it 'verifies request payload' do
+ signature = 'c4574c1748064735513697750c6223ff36b03ae3b85b160ce8788557d01e1d9d1c9cd942074323ee0061d3dcc8c94359c5acfa6eee8e2da095b3967b1a88ab73'
+ payload = '{"id":"4a9157ffbd18cfbd73a57298","type":"security-action","flow":{"id":"62298c73a9bb433fbd1f75984a9157fd","name":"Block user that violates geo velocity"},"userId":"73a9bb433fbd1f75984a9157","userTraits":{"name":"John Doe","email":"john.doe@gmail.com"},"request":{"ip":"10.0.0.0","fp":"9bb433fb984a9157d1f7598"},"action":"block","properties":{"type":"customer"},"timestamp":"2020-02-23T22:28:55.387Z"}'
+ secret_key = 'B00C42DAD33EAC6F6572DA756EA4915349C0A4F6'
+
+ expect(SignatureUtils.valid_signature?(secret_key, payload, signature)).to be_truthy
+ end
+
+ it 'verifies request empty signature' do
+ expect(SignatureUtils.valid_signature?('', '', 'B00C42DAD33EAC6F6572DA756EA4915349C0A4F6')).to be_falsey
+ end
+end
\ No newline at end of file
diff --git a/test/test_event_manager.rb b/test/test_event_manager.rb
deleted file mode 100644
index 7ac6fc7..0000000
--- a/test/test_event_manager.rb
+++ /dev/null
@@ -1,57 +0,0 @@
-require_relative '../lib/securenative/event_manager'
-require_relative '../lib/securenative/securenative_options'
-require_relative '../lib/securenative/event_type'
-require_relative '../lib/securenative/event_options'
-require 'rspec/autorun'
-require 'securerandom'
-
-class MockHttpClient
- def post(path, data, _)
- {
- :path => path,
- :data => data,
- :headers => {
- "Content-Type" => 'application/json',
- "User-Agent" => 'SecureNative-ruby',
- "Sn-Version" => Config::SDK_VERSION,
- "Authorization" => data
- }
- }
- end
-end
-
-def build_event(type = EventType::LOG_IN, id)
- Event.new(event_type = type,
- user: User.new(user_id: id, user_email: 'support@securenative.com', user_name: 'support'),
- params: [CustomParam.new('key', 'val')]
- )
-end
-
-describe EventManager do
- let(:api_key) {ENV["SN_API_KEY"]}
- let(:event_manager) {EventManager.new(api_key, options: SecureNativeOptions.new, http_client: MockHttpClient.new)}
-
- it "post a synced request" do
- user_id = SecureRandom.uuid
- url = "https://postman-echo.com/post"
- event = build_event(EventType::LOG_IN, user_id)
- item = event_manager.send_async(event, url)
- event_manager.flush
-
- expect(item).to be_instance_of(Thread::Queue)
- end
-
- it "post an a-synced request" do
- user_id = SecureRandom.uuid
- url = "https://postman-echo.com/post"
- event = build_event(EventType::LOG_IN, user_id)
-
- res = event_manager.send_sync(event, url)
- expect(res).not_to be_empty
- expect(res[:data]).to eq(api_key)
- expect(res[:headers]["Content-Type"]).to eq("application/json")
- expect(res[:headers]["User-Agent"]).to eq("SecureNative-ruby")
- expect(res[:headers]["Sn-Version"]).to eq(Config::SDK_VERSION)
- expect(res[:headers]["Authorization"]).to eq(api_key)
- end
-end
diff --git a/test/test_http_client.rb b/test/test_http_client.rb
deleted file mode 100644
index 3529468..0000000
--- a/test/test_http_client.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-require 'rspec/autorun'
-require 'json'
-require_relative '../lib/securenative/http_client'
-require_relative '../lib/securenative/config'
-
-describe HttpClient do
- let(:client) {HttpClient.new}
-
- it "post a request to postman-echo" do
- api_key = "some api key"
- url = "https://postman-echo.com/post"
- res = client.post(url, api_key, "")
-
- expect(res.status).to eq(200)
- res_body = JSON.parse(res.body)
- expect(res_body["headers"]['content-type']).to eq("application/json")
- expect(res_body["headers"]['user-agent']).to eq("SecureNative-ruby")
- expect(res_body["headers"]['sn-version']).to eq(Config::SDK_VERSION)
- expect(res_body["headers"]['authorization']).to eq(api_key)
- end
-end
\ No newline at end of file
diff --git a/test/test_sdk.rb b/test/test_sdk.rb
deleted file mode 100644
index cf2b88b..0000000
--- a/test/test_sdk.rb
+++ /dev/null
@@ -1,49 +0,0 @@
-require_relative '../lib/securenative'
-require_relative '../lib/securenative/securenative_options'
-require_relative '../lib/securenative/event_type'
-require_relative '../lib/securenative/event_options'
-require 'rspec/autorun'
-require 'securerandom'
-require 'json'
-
-def build_event(type = EventType::LOG_IN, id)
- Event.new(event_type = type,
- user: User.new(user_id: id, user_email: 'support@securenative.com', user_name: 'support'),
- params: [CustomParam.new('key', 'val')]
- )
-end
-
-describe SecureNativeSDK do
- let(:sn_options) {SecureNativeOptions.new}
- let(:sn) {nil}
-
- it "use sdk without api key" do
- api_key = nil
- expect {SecureNative.init(api_key, options: sn_options)}.to raise_error(SecureNativeSDKException)
- end
-
- it "check for singleton use" do
- sn = SecureNative.init(ENV["SN_API_KEY"], options: sn_options)
- expect {sn.init(ENV["SN_API_KEY"], options: sn_options)}.to raise_error(StandardError)
- end
-
- it "track an event" do
- sn = SecureNative._get_or_throw
- user_id = SecureRandom.uuid
- event = build_event(EventType::LOG_IN, user_id)
- sn.track(event)
- expect(sn.flush).not_to be_empty
- end
-
- it "verify an event" do
- sn = SecureNative._get_or_throw
- user_id = SecureRandom.uuid
- event = build_event(EventType::LOG_OUT, user_id)
- res = sn.verify(event)
-
- expect(res).not_to be_empty
- expect(res['triggers']).not_to be_empty
- expect(res['riskLevel']).to eq('high')
- expect(res['score']).to eq(0.64)
- end
-end
\ No newline at end of file
diff --git a/test/test_utils.rb b/test/test_utils.rb
deleted file mode 100644
index 0ca29a5..0000000
--- a/test/test_utils.rb
+++ /dev/null
@@ -1,48 +0,0 @@
-require 'rspec/autorun'
-require "base64"
-require_relative '../lib/securenative/utils'
-
-describe Utils do
- it "verifies a signature" do
- secret = "super secretive secret"
- body = "random body"
- true_signature = "acfd06a839604c12d21649508c1b3fa25d84e043d2b1e26b5967b9f9883184227c80c249decd9229205c7d072d2c7dc0f5c525cf4c14209d34f72306de97d50c"
- false_signature = "c80c249decd9229205c7d072d2c7dc0f5c525cf4c14209d34f72306de97d50cacfd06a839604c12d21649508c1b3fa25d84e043d2b1e26b5967b9f9883184227"
- expect(Utils.verify_signature(secret, body, true_signature)).to eq(true)
- expect(Utils.verify_signature(secret, body, false_signature)).to eq(false)
- end
-
- it "successfully parse a cookie" do
- cookie = Utils.parse_cookie(Base64.encode64('{"fp": fp, "cid": cid}'))
- expect(cookie).to_not be_empty
- expect(cookie[0]).to eq("fp")
- expect(cookie[1]).to eq("cid")
- end
-
- it "fails at parsing a cookie" do
- cookie = Utils.parse_cookie
- expect(cookie).to_not be_empty
- expect(cookie[0]).to be_empty
- expect(cookie[1]).to be_empty
- end
-
- it "encrypt text" do
- plain_text = "Some random plain text for test"
- key = "6EA4915349C0AAC6F6572DA4F6B00C42DAD33E75"
- encrypted = "B8D770D0F7388EC3DF62B0097C36C05CAB03E934F2E2760536004F87FE11644C"
-
- res = Utils.encrypt(plain_text, key)
- expect(res).to_not be_empty
- expect(res).to eq(encrypted)
- end
-
- it "decrypt text" do
- encrypted_text = "B8D770D0F7388EC3DF62B0097C36C05CAB03E934F2E2760536004F87FE11644C"
- decrypted = "Some random plain text for test"
- key = "6EA4915349C0AAC6F6572DA4F6B00C42DAD33E75"
-
- res = Utils.decrypt(encrypted_text, key)
- expect(res).to_not be_empty
- expect(res).to eq(decrypted)
- end
-end
\ No newline at end of file