prevoty-rails 0.6.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +45 -0
- data/LICENSE.txt +22 -0
- data/README.md +67 -0
- data/Rakefile +2 -0
- data/lib/action_controller/request_forgery_protection.rb +36 -0
- data/lib/generators/prevoty/rails/USAGE +9 -0
- data/lib/generators/prevoty/rails/install_generator.rb +13 -0
- data/lib/generators/prevoty/rails/templates/prevoty_rails.rb +86 -0
- data/lib/prevoty-rails.rb +21 -0
- data/lib/prevoty/adapters.rb +8 -0
- data/lib/prevoty/build_query_result.rb +20 -0
- data/lib/prevoty/content_monitor.rb +29 -0
- data/lib/prevoty/content_payload.rb +48 -0
- data/lib/prevoty/logger.rb +5 -0
- data/lib/prevoty/monitor.rb +53 -0
- data/lib/prevoty/query_failure.rb +4 -0
- data/lib/prevoty/query_monitor.rb +37 -0
- data/lib/prevoty/query_payload.rb +26 -0
- data/lib/prevoty/query_violation.rb +4 -0
- data/lib/prevoty/rails/version.rb +5 -0
- data/lib/rack/content_middleware.rb +22 -0
- data/lib/rack/interceptor.rb +61 -0
- data/lib/rack/monitor_interceptor.rb +51 -0
- data/lib/rack/protect_interceptor.rb +93 -0
- data/prevoty-rails.gemspec +29 -0
- metadata +155 -0
checksums.yaml
ADDED
@@ -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
data/Gemfile.lock
ADDED
@@ -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)
|
data/LICENSE.txt
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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
|
data/Rakefile
ADDED
@@ -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,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,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,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,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,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:
|