prevoty-rails 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 48d11a869e27de35006a6d786687f2fa71e7c06b
4
+ data.tar.gz: dde0330df03439b25e19366c398c197613cac55d
5
+ SHA512:
6
+ metadata.gz: e331e0d3dafb950a791a0fbee556d814a586d376d78e2cfd9b1ea8fb3b719e09c267fc9b3dd88b48456e5244b1dc54a81909ea134668ea28d8f8142d035b599c
7
+ data.tar.gz: a16b402f892a9fd125505446805600b63000024a6470fd7c6f00b5e19f8835d3d10933dd54412fe4d3255579f06c61f6f726f6d88cf17b0363008cd90fb1d20e
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
@@ -0,0 +1,45 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ prevoty-rails (0.1.0)
5
+ prevoty (~> 1.0)
6
+ rack (~> 1.4)
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ byebug (3.5.1)
12
+ columnize (~> 0.8)
13
+ debugger-linecache (~> 1.2)
14
+ slop (~> 3.6)
15
+ coderay (1.1.0)
16
+ columnize (0.9.0)
17
+ debugger-linecache (1.2.0)
18
+ httparty (0.13.3)
19
+ json (~> 1.8)
20
+ multi_xml (>= 0.5.2)
21
+ json (1.8.2)
22
+ method_source (0.8.2)
23
+ multi_xml (0.5.5)
24
+ prevoty (1.0.1)
25
+ httparty (~> 0.13)
26
+ pry (0.10.1)
27
+ coderay (~> 1.1.0)
28
+ method_source (~> 0.8.1)
29
+ slop (~> 3.4)
30
+ pry-byebug (3.0.1)
31
+ byebug (~> 3.4)
32
+ pry (~> 0.10)
33
+ rack (1.6.0)
34
+ rake (10.4.2)
35
+ slop (3.6.0)
36
+
37
+ PLATFORMS
38
+ ruby
39
+
40
+ DEPENDENCIES
41
+ bundler (~> 1.7)
42
+ prevoty-rails!
43
+ pry (~> 0.10)
44
+ pry-byebug (~> 3.0)
45
+ rake (~> 10.0)
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Prevoty
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,67 @@
1
+ # prevoty-rails
2
+
3
+ prevoty-rails is a plugin to automatically integrate Prevoty's content filtering and SQL anlysis engine into a Rails application. The content filter is distributed as a Rack middleware that can be added into the request chain and the SQL analysis is handled using the ActiveSupport notification system to inspect SQL queries before they are sent to the database.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'prevoty-rails'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install prevoty-rails
20
+
21
+ ### Content Filter
22
+ Add the following line into your config/application.rb file to use the Prevoty content filter in all environments or add it to the specific environment (eg. testing/production) that you would like to use it with.
23
+
24
+ ```ruby
25
+ config.middleware.use Rack::Prevoty::ContentMiddleware, {api_key: '', configuration_key: ''}
26
+ ```
27
+
28
+ ### SQL Analysis
29
+ First use the generator to install the initializer needed for SQL.
30
+
31
+ ```shell
32
+ rails generate prevoty:rails:install
33
+ ```
34
+
35
+ This will create a new file at config/initializers/prevoty_rails.rb. Fill in the configuration key for the query config created in the Prevoty console and the API key to use with it.
36
+
37
+ ## Configuration
38
+
39
+ ### Common
40
+ - `api_key`: Prevoty v1 API key (shown at the bottom of the API Keys page in Prevoty Manager Console)
41
+ - `api_base`: Base api url (default: 'https://api.prevoty.com')
42
+ - `api_timeout_milliseconds`: Timeout for api requests (default: 1000)
43
+
44
+ ### Content Filter
45
+ - `mode`: ['monitor' | 'protect'] Content mode (default: monitor)
46
+ - `paths`: Array of uris that the content filter should be applied to. These MAY be partial uris but MUST be defined from the beginning of the uri (default: [])
47
+ - `blacklist`: Array of uris that the content filter should ignore that are within the paths. These MAY be partial uris but MUST be defined from the beginning of the uri (default: [])
48
+ - `minimal_logging`: [true | false] Log minimal events (default: false)
49
+ - `destination`: ['log' | 'callback' | 'none'] Logging destination (default: log)
50
+ - `callback`: Proc object to call when log_destination is set to callback
51
+ - `configuration_key`: Configuration Policy Key for content filtering (from the Security Policies page in Prevoty Manager Console, not to be confused with the keys from the Plugin Configurations page)
52
+ - `reporting_milliseconds`: Interval data will have monitoring analysis performed when the mode is set to monitor and the queue is not filled (default: 10000)
53
+ - `reporting_count`: Maximum queue size for data to be sent for monitoring analysis (default: 50)
54
+
55
+ ### SQL Analysis
56
+ - `mode`: ['monitor' | 'protect'] query analysis mode (default: monitor)
57
+ - `minimal_logging`: [true | false] Log minimal events (default: false)
58
+ - `log_destination`: ['log' | 'callback' | 'none'] Logging destination (default: log)
59
+ - `configuration_key`: Configuration key for sql analysis (from the Security Policies page in Prevoty Manager Console, not to be confused with the keys from the Plugin Configurations page)
60
+ - `reporting_milliseconds`: Interval data will have monitoring analysis performed when the query_mode is set to monitor and the queue is not filled (default: 10000)
61
+ - `reporting_count`: Maximum queue size for data to be sent for monitoring analysis (default: 50)
62
+ - `db_vendor`: ['mysql' | 'mssql' | 'oracle'] The database vendor the application is using for monitoring
63
+ - `db_version`: The version of the database being used for monitoring
64
+ - `db_name`: The default database to use for monitoring
65
+ - `violation_mode`: ['block' | 'continue'] Determines whether to stop the request or let it continue if there is a violation when sql analysis is in 'protect' mode (default: 'continue')
66
+ - `failure_mode`: ['block' | 'continue'] Determines whether to stop the request or let it continue if there is an error when sql analysis is in 'protect' mode (default: 'continue')
67
+ - `callback`: Proc object to call when log_destination is set to callback
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,36 @@
1
+ module ActionController
2
+ module RequestForgeryProtection
3
+ # NOTE: This implementation currently doesn't support the BREACH
4
+ # mitigation that Rails introduces with masking. The Prevoty
5
+ # implementation possibly makes this unnecessary because it scopes CSRF
6
+ # tokens by action and not just the session id. This allows for each form
7
+ # to have a unique token and can be re-generated for each request. However
8
+ # because of how the plugin works there isn't a clear way to set the action
9
+ # so all forms will share a token. We should either introduce masking or
10
+ # find a solution to setting the action correctly.
11
+ protected
12
+ def form_authenticity_token
13
+ begin
14
+ resp = PREVOTY_CLIENT.generate_timed_token(session[:session_id], "action", 86400)
15
+ session[:_csrf_token] = resp.token
16
+ rescue Exception => e
17
+ Rails.logger.debug e.message
18
+ end
19
+ end
20
+
21
+ def verified_request?
22
+ return true if !protect_against_forgery? || request.get? || request.head?
23
+ begin
24
+ resp = PREVOTY_CLIENT.validate_timed_token(session[:session_id], "action", session[:_csrf_token])
25
+ return true if resp.valid
26
+ resp = PREVOTY_CLIENT.validate_timed_token(session[:session_id], "action", request.headers['X-CSRF-Token'])
27
+ return true if resp.valid
28
+ rescue
29
+ Rails.logger.debug e.message
30
+ return false
31
+ end
32
+
33
+ false
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,9 @@
1
+ Description:
2
+ Generates the base initializer for the CSRF and SQL query support of
3
+ Prevoty's Ruby plugin.
4
+
5
+ Examples:
6
+ rails generate prevoty:rails:install
7
+
8
+ This will generate the base initializer for the Prevoty ruby plugin for
9
+ your rails app.
@@ -0,0 +1,13 @@
1
+ require 'rails/generators'
2
+
3
+ module Prevoty
4
+ module Rails
5
+ class InstallGenerator < ::Rails::Generators::Base
6
+ source_root File.expand_path('../templates', __FILE__)
7
+
8
+ def copy_initializer_file
9
+ template "prevoty_rails.rb", "config/initializers/prevoty_rails.rb"
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,86 @@
1
+ require 'prevoty/query_violation'
2
+ require 'prevoty/query_failure'
3
+ require 'prevoty/query_payload'
4
+ require 'prevoty/build_query_result'
5
+ require 'prevoty/logger'
6
+
7
+ options = {
8
+ api_base: 'https://api.prevoty.com',
9
+ api_key: '',
10
+ mode: 'monitor',
11
+ minimal_logging: false,
12
+ log_destination: 'log',
13
+ # configuration_key: '', # uncomment for protect
14
+ reporting_milliseconds: 10000,
15
+ reporting_count: 50,
16
+ db_vendor: '', # unused for protect
17
+ db_version: '', # unused for protect
18
+ db_name: '', # unused for protect
19
+ violation_mode: 'block',
20
+ failure_mode: 'continue',
21
+ before_callback: ->() {}, # fired before a query is analyzed
22
+ after_callback: ->() {}, # fired after a query is analyzed
23
+ error_callback: ->() {} # fired after an error
24
+ }
25
+
26
+ case options[:mode]
27
+ when 'monitor'
28
+ client = ::Prevoty::Client.new(options[:api_key], options[:api_base])
29
+ QUERY_MONITOR = ::Prevoty::QueryMonitor.new client, options
30
+ when 'protect'
31
+ PREVOTY_CLIENT = ::Prevoty::Client.new(options[:api_key], options[:api_base])
32
+ end
33
+
34
+ query_handler = ->(name, start, finish, id, payload) do
35
+ include Prevoty::BuildQueryResult
36
+
37
+ unless payload[:name] =~ /SQL|Load/
38
+ return
39
+ end
40
+
41
+ case options[:mode]
42
+ when 'monitor'
43
+ package = {vendor: options[:db_vendor], database: options[:db_name], version: options[:db_version], query: payload[:sql]}
44
+ QUERY_MONITOR.process(package)
45
+ when 'protect'
46
+ begin
47
+ options[:before_callback].call(payload[:sql]) if options[:log_destination] === 'callback' and options[:before_callback].respond_to? :call
48
+ res = PREVOTY_CLIENT.analyze_query(payload[:sql], options[:configuration_key])
49
+ if res.processed and not res.compliant
50
+ case options[:log_destination]
51
+ when 'log'
52
+ ::Prevoty::LOGGER << build_result(options[:mode], payload[:sql], res).to_json
53
+ when 'callback'
54
+ options[:after_callback].call(build_result(options[:mode], payload[:sql], res).to_json) if options[:after_callback].respond_to? :call
55
+ end
56
+ raise ::Prevoty::QueryViolation.new if options[:violation_mode] === 'block'
57
+ elsif res.processed and res.compliant
58
+ case options[:log_destination]
59
+ when 'log'
60
+ ::Prevoty::LOGGER << build_result(options[:mode], payload[:sql], res).to_json
61
+ when 'callback'
62
+ options[:after_callback].call(build_result(options[:mode], payload[:sql], res).to_json) if options[:after_callback].respond_to? :call
63
+ end
64
+ elsif not res.processed and not res.compliant
65
+ raise ::Prevoty::QueryFailure.new
66
+ end
67
+ rescue ::Prevoty::QueryViolation => e
68
+ # need to catch and rethrow to catch other exceptions
69
+ raise e
70
+ rescue ::Prevoty::QueryFailure => e
71
+ # need to catch and rethrow to catch other exceptions
72
+ raise e
73
+ rescue Exception => e
74
+ case options[:log_destination]
75
+ when 'log'
76
+ Rails.logger.error e.message
77
+ when 'callback'
78
+ options[:error_callback].call(e) if options[:error_callback].response_to_? :call
79
+ end
80
+
81
+ raise e if options[:failure_mode] === 'block'
82
+ end
83
+ end
84
+ end
85
+
86
+ ActiveSupport::Notifications.subscribe 'sql.active_record', query_handler
@@ -0,0 +1,21 @@
1
+ require 'prevoty/adapters'
2
+ require 'prevoty/monitor'
3
+ require 'prevoty/content_monitor'
4
+ require 'prevoty/query_monitor'
5
+ require 'prevoty/query_payload'
6
+ require 'prevoty/query_violation'
7
+ require 'prevoty/query_failure'
8
+ require 'prevoty/build_query_result'
9
+ require 'prevoty/rails/version'
10
+ require 'prevoty/logger'
11
+ require 'rack/content_middleware'
12
+
13
+ # Uncommenting this enables Prevoty CSRF protection that overloads the
14
+ # built-in Rails implementation. There isn't a good way to specify the action
15
+ # since we don't know where the form will POST to at the point we're hooking
16
+ # so it makes the token less secure given that they all share a common action.
17
+ # Rails generates a token and uses it session wide then masks it using a one
18
+ # time pad and xor so that each time it's on the page it appears different to
19
+ # mitigate against the BREACH SSL attack. This essentially creates a unique
20
+ # token for each request which is the approach Prevoty takes.
21
+ # require 'action_controller/request_forgery_protection'
@@ -0,0 +1,8 @@
1
+ module Prevoty
2
+ ADAPTERS = {
3
+ "mysql" => "mysql",
4
+ "mysql2" => "mysql",
5
+ "oracle" => "oracle",
6
+ "mssql" => "mssql"
7
+ }
8
+ end
@@ -0,0 +1,20 @@
1
+ require 'prevoty/query_payload'
2
+
3
+ module Prevoty
4
+ module BuildQueryResult
5
+ def build_result(mode, input, result)
6
+ data = {
7
+ timestamp: Time.now.utc.strftime('%b %d %Y %H:%M:%S %Z'),
8
+ product: 'query',
9
+ mode: mode,
10
+ version: 1,
11
+ runtime_version: result.engine_version,
12
+ processed: result.processed,
13
+ input: input,
14
+ result: result
15
+ }
16
+
17
+ Prevoty::QueryPayload.new(data)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,29 @@
1
+ require 'thread'
2
+
3
+ module Prevoty
4
+ class ContentMonitor < Monitor
5
+ private
6
+ def process_queue
7
+ cloned = @queue.clone
8
+ @queue = []
9
+ Thread.new do
10
+ data = cloned.inject([]) {|arr, i| arr.push i[:input]; arr}
11
+ begin
12
+ Timeout::timeout(@timeout) do
13
+ res = @client.monitor_content(data)
14
+ res.each_with_index do |r, i|
15
+ case @log_destination
16
+ when 'log'
17
+ ::Prevoty::LOGGER << ::Rack::Prevoty::Interceptor.build_result(cloned[i][:mode], cloned[i][:request], cloned[i][:input], r).to_json if r.javascript_attributes > 0 || r.javascript_protocols > 0 || r.javascript_tags > 0
18
+ when 'callback'
19
+ @callback.call(::Rack::Prevoty::Interceptor.build_result(cloned[i][:mode], cloned[i][:request], cloned[i][:input], r).to_json) if !@callback.nil? && (r.javascript_attributes > 0 || r.javascript_protocols > 0 || r.javascript_tags > 0)
20
+ end
21
+ end
22
+ end
23
+ rescue Exception => e
24
+ ::Rails.logger.warn e.message
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,48 @@
1
+ require 'json'
2
+
3
+ module Prevoty
4
+ class ContentPayload
5
+ attr_accessor :timestamp, :product, :mode, :version, :input, :output,
6
+ :statistics, :request_url, :session_id, :cookies,
7
+ :http_method, :src_ip, :dest_host, :dest_port
8
+
9
+ def initialize(data)
10
+ @timestamp = data[:timestamp]
11
+ @product = data[:product]
12
+ @mode = data[:mode]
13
+ @version = data[:version]
14
+ @input = data[:input]
15
+ @output = data[:output]
16
+ @statistics = data[:statistics]
17
+ @request_url = data[:request_url]
18
+ @session_id = data[:session_id]
19
+ @cookies = data[:cookies]
20
+ @http_method = data[:http_method]
21
+ @src_ip = data[:src_ip]
22
+ @dest_host = data[:dest_host]
23
+ @dest_port = data[:dest_port]
24
+ end
25
+
26
+ def to_json
27
+ case @mode
28
+ when 'monitor'
29
+ return {
30
+ timestamp: @timestamp, product: @product, mode: @mode,
31
+ version: @version, input: @input, statistics: @statistics,
32
+ url: @request_url, session_id: @session_id, cookies: @cookies,
33
+ http_method: @http_method, src_ip: @src_ip,
34
+ dest_host: @dest_host, dest_port: @dest_port
35
+ }.to_json
36
+ when 'protect'
37
+ return {
38
+ timestamp: @timestamp, product: @product, mode: @mode,
39
+ version: @version, input: @input, output: @output,
40
+ statistics: @statistics, url: @request_url,
41
+ session_id: @session_id, cookies: @cookies,
42
+ http_method: @http_method, src_ip: @src_ip,
43
+ dest_host: @dest_host, dest_port: @dest_port
44
+ }.to_json
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,5 @@
1
+ require 'logger'
2
+
3
+ module Prevoty
4
+ LOGGER = Logger.new(File.join(Dir.pwd, 'log', 'prevoty_splunk.log'))
5
+ end
@@ -0,0 +1,53 @@
1
+ require 'timeout'
2
+ require 'thread'
3
+
4
+ module Prevoty
5
+ class Monitor
6
+ DEFAULT_TIMEOUT = 10000
7
+ DEFAULT_MAX_SIZE = 50
8
+ DEFAULT_LOG_DESTINATION = 'log'
9
+
10
+ def initialize(client, options = {})
11
+ options[:reporting_milliseconds] ||= DEFAULT_TIMEOUT
12
+
13
+ @client = client
14
+ @before_callback = options[:before_callback]
15
+ @after_callback = options[:after_callback]
16
+ @max_size = options[:max_size] || DEFAULT_MAX_SIZE
17
+ @log_destination = options[:log_destination] || DEFAULT_LOG_DESTINATION
18
+ @timeout = options[:reporting_milliseconds] / 1000
19
+ @mutex = Mutex.new
20
+ @queue = []
21
+ @rd, @wr = IO.pipe
22
+ @thread = Thread.new do
23
+ loop do
24
+ begin
25
+ Timeout::timeout(@timeout) { @rd.read }
26
+ redo
27
+ rescue Timeout::Error
28
+ @mutex.lock()
29
+ if @queue.size > 0
30
+ process_queue
31
+ end
32
+ @mutex.unlock()
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ def process(data)
39
+ @mutex.lock
40
+ @queue.push(data)
41
+ if @queue.size >= @max_size
42
+ process_queue
43
+ @wr.write 1
44
+ end
45
+ @mutex.unlock
46
+ end
47
+
48
+ private
49
+ def process_queue
50
+ raise 'this is an abstract base method. You should use the ContentMonitor or QueryMonitor class instead'
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,4 @@
1
+ module Prevoty
2
+ class QueryFailure < Exception
3
+ end
4
+ end
@@ -0,0 +1,37 @@
1
+ require 'thread'
2
+ require 'prevoty/build_query_result'
3
+
4
+ module Prevoty
5
+ class QueryMonitor < Monitor
6
+ include Prevoty::BuildQueryResult
7
+
8
+ private
9
+ def process_queue
10
+ cloned = @queue.clone
11
+ @queue = []
12
+ Thread.new do
13
+ begin
14
+ @before_callback.call(cloned) if @log_destination === 'callback' and @before_callback.respond_to? :call
15
+ Timeout::timeout(@timeout) do
16
+ res = @client.monitor_query(cloned)
17
+ res.each_with_index do |r, i|
18
+ case @log_destination
19
+ when 'log'
20
+ Prevoty::LOGGER << build_result('monitor', cloned[i][:query], r).to_json if r.processed
21
+ when 'callback'
22
+ @after_callback.call(build_result('monitor', cloned[i][:query], r).to_json) if @after_callback.respond_to?(:call) && r.processed
23
+ end
24
+ end
25
+ end
26
+ rescue Exception => e
27
+ case @log_destination
28
+ when 'log'
29
+ ::Rails.logger.error e.message
30
+ when 'callback'
31
+ @error_callback.call(e.message) if @error_callback.respond_to? :call
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,26 @@
1
+ require 'json'
2
+
3
+ module Prevoty
4
+ class QueryPayload
5
+ attr_accessor :product, :mode, :version, :runtime_version, :processed,
6
+ :input, :result, :timestamp
7
+
8
+ def initialize(data)
9
+ @product = data[:product]
10
+ @mode = data[:mode]
11
+ @version = data[:version]
12
+ @runtime_version = data[:runtime_version]
13
+ @processed = data[:processed]
14
+ @input = data[:input]
15
+ @result = data[:result]
16
+ @timestamp = data[:timestamp]
17
+ end
18
+
19
+ def to_json
20
+ return {
21
+ product: @product, mode: @mode, version: @version,
22
+ input: @input, result: @result, timestamp: @timestamp
23
+ }.to_json
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,4 @@
1
+ module Prevoty
2
+ class QueryViolation < Exception
3
+ end
4
+ end
@@ -0,0 +1,5 @@
1
+ module Prevoty
2
+ module Rails
3
+ VERSION = '0.6.1'
4
+ end
5
+ end
@@ -0,0 +1,22 @@
1
+ require 'rack/monitor_interceptor'
2
+ require 'rack/protect_interceptor'
3
+
4
+ module Rack
5
+ module Prevoty
6
+ class ContentMiddleware
7
+ def initialize(app, opts)
8
+ case opts[:mode]
9
+ when 'protect' then @interceptor = Rack::Prevoty::ProtectInterceptor.new(app, opts)
10
+ when 'monitor' then @interceptor = Rack::Prevoty::MonitorInterceptor.new(app, opts)
11
+ else
12
+ @interceptor = MonitorInterceptor.new(app, opts)
13
+ Rails.logger.warn "invalid mode #{opts[:mode]} specified; using default: monitor"
14
+ end
15
+ end
16
+
17
+ def call(env)
18
+ @interceptor.call(env)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,61 @@
1
+ require 'prevoty'
2
+ require 'prevoty/content_payload'
3
+
4
+ module Rack
5
+ module Prevoty
6
+ class Interceptor
7
+ def initialize(app, opts)
8
+ @app = app
9
+ @base = opts[:api_base] ||= 'https://api.prevoty.com'
10
+ @client = ::Prevoty::Client.new(opts[:api_key], @base)
11
+ @configuration_key = opts[:configuration_key]
12
+ @mode = opts[:mode] ||= 'monitor'
13
+ @callback = opts[:callback] ||= nil
14
+ @minimal_logging = opts[:minimal_logging] ||= false
15
+ @log_destination = opts[:log_destination] ||= 'log'
16
+ @paths = opts[:paths] ||= ['/']
17
+ @blacklist = opts[:blacklist] ||= []
18
+ end
19
+
20
+ def call(env)
21
+ raise 'this is an abstract class...instantiate one of the child rack middlewares'
22
+ end
23
+
24
+ def self.build_result(mode, request, input, result)
25
+ data = {
26
+ product: 'content',
27
+ mode: mode,
28
+ version: '1',
29
+ input: input,
30
+ timestamp: Time.now.utc.strftime('%b %d %Y %H:%M:%S %Z'),
31
+ request_url: request.path,
32
+ session_id: request.session["session_id"],
33
+ cookies: request.cookies,
34
+ http_method: request.request_method,
35
+ src_ip: request.ip,
36
+ dest_host: request.host,
37
+ dest_port: request.port
38
+ }
39
+
40
+ # these are hacks due to differences between protect and monitor
41
+ if mode === 'protect'
42
+ data[:statistics] = result.statistics
43
+ data[:output] = CGI::unescape(result.output)
44
+ else
45
+ data[:statistics] = result
46
+ end
47
+
48
+ ::Prevoty::ContentPayload.new(data)
49
+ end
50
+
51
+ protected
52
+ def escape_query(params)
53
+ params.map do |name,values|
54
+ values.map do |value|
55
+ "#{CGI.escape name}=#{CGI.escape CGI.escapeHTML value}"
56
+ end
57
+ end.flatten.join("&")
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,51 @@
1
+ require 'rack/interceptor'
2
+
3
+ module Rack
4
+ module Prevoty
5
+ class MonitorInterceptor < Interceptor
6
+ def initialize(app, opts)
7
+ super(app, opts)
8
+
9
+ @monitor = ::Prevoty::ContentMonitor.new(@client, opts)
10
+ end
11
+
12
+ def call(env)
13
+ req = Rack::Request.new(env)
14
+
15
+ # passthru if not listed in paths
16
+ return @app.call(env) if @paths.detect {|p| req.path.start_with?(p)}.nil?
17
+
18
+ # passthru if blacklisted
19
+ return @app.call(env) unless @blacklist.detect {|p| req.path.start_with?(p)}.nil?
20
+
21
+ case req.request_method
22
+ when "GET", "DELETE"
23
+ unless env['QUERY_STRING'] === ''
24
+ querystring = URI.unescape(env['QUERY_STRING'])
25
+ @monitor.process({mode: @mode, input: querystring, request: req})
26
+ end
27
+ when "POST", "PUT"
28
+ if req.media_type === 'multipart/form-data'
29
+ # TODO: implement support for multipart. The Rack multipart
30
+ # implementation doesn't support parsing and re-creating the
31
+ # mutlipart data so a custom implementation needs to be written
32
+ else
33
+ body = URI.unescape(req.body.read.encode('utf-8'))
34
+ unless body === ''
35
+ @monitor.process({mode: @mode, input: body, request: req})
36
+ end
37
+ end
38
+
39
+ # clean any GET data passed
40
+ unless env['QUERY_STRING'] === ''
41
+ querystring = URI.unescape(env['QUERY_STRING'])
42
+ @monitor.process({mode: @mode, input: querystring, request: req})
43
+ end
44
+ else Rails.logger.warn "unknown method #{req.request_method}"
45
+ end
46
+
47
+ @app.call(env)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,93 @@
1
+ require 'timeout'
2
+ require 'cgi'
3
+ require 'rack/interceptor'
4
+
5
+ module Rack
6
+ module Prevoty
7
+ class ProtectInterceptor < Interceptor
8
+ def initialize(app, opts)
9
+ super(app, opts)
10
+ end
11
+
12
+ def call(env)
13
+ req = Rack::Request.new(env)
14
+
15
+ # passthru if not listed in paths
16
+ return @app.call(env) if @paths.detect {|p| req.path.start_with?(p)}.nil?
17
+
18
+ # passthru if blacklisted
19
+ return @app.call(env) unless @blacklist.detect {|p| req.path.start_with?(p)}.nil?
20
+
21
+ case req.request_method
22
+ when "GET", "DELETE"
23
+ unless env['QUERY_STRING'] === ''
24
+ querystring = URI.unescape(env['QUERY_STRING'])
25
+ begin
26
+ Timeout::timeout(@timeout) do
27
+ resp = @client.bulk_filter(querystring, @configuration_key)
28
+ env['QUERY_STRING'] = URI.escape(resp.output)
29
+ case @log_destination
30
+ when 'log'
31
+ ::Prevoty::LOGGER << self.class.build_result(@mode, req, querystring, resp).to_json if resp.statistics.javascript_attributes > 0 || resp.statistics.javascript_protocols > 0 || resp.statistics.javascript_tags > 0
32
+ when 'callback'
33
+ @callback.call(self.class.build_result(@mode, req, querystring, resp).to_json) if !@callback.nil? && (resp.statistics.javascript_attributes > 0 || resp.statistics.javascript_protocols > 0 || resp.statistics.javascript_tags > 0)
34
+ end
35
+ end
36
+ rescue Exception => e
37
+ env['QUERY_STRING'] = escape_query(CGI::parse(querystring))
38
+ Rails.logger.warn e.message
39
+ end
40
+ end
41
+ when "POST", "PUT"
42
+ if req.media_type === 'multipart/form-data'
43
+ # TODO: implement support for multipart. The Rack multipart
44
+ # implementation doesn't support parsing and re-creating the
45
+ # mutlipart data so a custom implementation needs to be written
46
+ else
47
+ body = URI.unescape(req.body.read.encode('utf-8'))
48
+ unless body === ''
49
+ begin
50
+ Timeout::timeout(@timeout) do
51
+ resp = @client.bulk_filter(body, @configuration_key)
52
+ env['rack.input'] = StringIO.new(resp.output)
53
+ case @log_destination
54
+ when 'log'
55
+ ::Prevoty::LOGGER << self.class.build_result(@mode, req, body, resp).to_json if resp.statistics.javascript_attributes > 0 || resp.statistics.javascript_protocols > 0 || resp.statistics.javascript_tags > 0
56
+ when 'callback'
57
+ @callback.call(self.class.build_result(@mode, req, body, resp).to_json) if !@callback.nil? && (resp.statistics.javascript_attributes > 0 || resp.statistics.javascript_protocols > 0 || resp.statistics.javascript_tags > 0)
58
+ end
59
+ end
60
+ rescue Exception => e
61
+ env['rack.input'] = StringIO.new(escape_query(CGI::parse(body)))
62
+ Rails.logger.warn e.message
63
+ end
64
+ end
65
+ end
66
+
67
+ # clean any GET data passed
68
+ unless env['QUERY_STRING'] === ''
69
+ querystring = URI.unescape(env['QUERY_STRING'])
70
+ begin
71
+ Timeout::timeout(@timeout) do
72
+ resp = @client.bulk_filter(querystring, @configuration_key)
73
+ env['QUERY_STRING'] = URI.escape(resp.output)
74
+ case @log_destination
75
+ when 'log'
76
+ ::Prevoty::LOGGER << self.class.build_result(@mode, req, querystring, resp).to_json if resp.statistics.javascript_attributes > 0 || resp.statistics.javascript_protocols > 0 || resp.statistics.javascript_tags > 0
77
+ when 'callback'
78
+ @callback.call(self.class.build_result(@mode, req, querystring, resp).to_json) if !@callback.nil? && (resp.statistics.javascript_attributes > 0 || resp.statistics.javascript_protocols > 0 || resp.statistics.javascript_tags > 0)
79
+ end
80
+ end
81
+ rescue Exception => e
82
+ env['QUERY_STRING'] = escape_query(CGI::parse(querystring))
83
+ Rails.logger.warn e.message
84
+ end
85
+ end
86
+ else Rails.logger.warn "unknown method #{req.request_method}"
87
+ end
88
+
89
+ @app.call(env)
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'prevoty/rails/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "prevoty-rails"
8
+ spec.version = Prevoty::Rails::VERSION
9
+ spec.authors = ["Joe Rozner"]
10
+ spec.email = ["joe@deadbytes.net"]
11
+ spec.summary = %q{Prevoty rails plugin.}
12
+ spec.description = %q{Plugin to add dropin support for Prevoty's content filter and SQL analysis.}
13
+ spec.homepage = "https://www.prevoty.com"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "prevoty", "~> 1.2"
22
+ spec.add_dependency "rack", "~> 1.4"
23
+ # spec.add_dependency "actionpack" only for CSRF
24
+
25
+ spec.add_development_dependency "bundler", "~> 1.7"
26
+ spec.add_development_dependency "rake", "~> 10.0"
27
+ spec.add_development_dependency "pry", "~> 0.10"
28
+ spec.add_development_dependency "pry-byebug", "~> 3.0"
29
+ end
metadata ADDED
@@ -0,0 +1,155 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: prevoty-rails
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.6.1
5
+ platform: ruby
6
+ authors:
7
+ - Joe Rozner
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-08-04 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: prevoty
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rack
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.4'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.4'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.7'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.7'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '10.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '10.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: pry
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.10'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.10'
83
+ - !ruby/object:Gem::Dependency
84
+ name: pry-byebug
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '3.0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '3.0'
97
+ description: Plugin to add dropin support for Prevoty's content filter and SQL analysis.
98
+ email:
99
+ - joe@deadbytes.net
100
+ executables: []
101
+ extensions: []
102
+ extra_rdoc_files: []
103
+ files:
104
+ - Gemfile
105
+ - Gemfile.lock
106
+ - LICENSE.txt
107
+ - README.md
108
+ - Rakefile
109
+ - lib/action_controller/request_forgery_protection.rb
110
+ - lib/generators/prevoty/rails/USAGE
111
+ - lib/generators/prevoty/rails/install_generator.rb
112
+ - lib/generators/prevoty/rails/templates/prevoty_rails.rb
113
+ - lib/prevoty-rails.rb
114
+ - lib/prevoty/adapters.rb
115
+ - lib/prevoty/build_query_result.rb
116
+ - lib/prevoty/content_monitor.rb
117
+ - lib/prevoty/content_payload.rb
118
+ - lib/prevoty/logger.rb
119
+ - lib/prevoty/monitor.rb
120
+ - lib/prevoty/query_failure.rb
121
+ - lib/prevoty/query_monitor.rb
122
+ - lib/prevoty/query_payload.rb
123
+ - lib/prevoty/query_violation.rb
124
+ - lib/prevoty/rails/version.rb
125
+ - lib/rack/content_middleware.rb
126
+ - lib/rack/interceptor.rb
127
+ - lib/rack/monitor_interceptor.rb
128
+ - lib/rack/protect_interceptor.rb
129
+ - prevoty-rails.gemspec
130
+ homepage: https://www.prevoty.com
131
+ licenses:
132
+ - MIT
133
+ metadata: {}
134
+ post_install_message:
135
+ rdoc_options: []
136
+ require_paths:
137
+ - lib
138
+ required_ruby_version: !ruby/object:Gem::Requirement
139
+ requirements:
140
+ - - ">="
141
+ - !ruby/object:Gem::Version
142
+ version: '0'
143
+ required_rubygems_version: !ruby/object:Gem::Requirement
144
+ requirements:
145
+ - - ">="
146
+ - !ruby/object:Gem::Version
147
+ version: '0'
148
+ requirements: []
149
+ rubyforge_project:
150
+ rubygems_version: 2.5.1
151
+ signing_key:
152
+ specification_version: 4
153
+ summary: Prevoty rails plugin.
154
+ test_files: []
155
+ has_rdoc: