wafris 1.1.10 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/wafris/configuration.rb +121 -37
- data/lib/wafris/log_suppressor.rb +3 -2
- data/lib/wafris/middleware.rb +45 -14
- data/lib/wafris/version.rb +1 -1
- data/lib/wafris.rb +582 -38
- metadata +44 -19
- data/lib/lua/dist/wafris_core.lua +0 -305
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f0dfe47e533d91f974d391af755f1bd8e148808cbfe86dd6755b839201282b47
|
4
|
+
data.tar.gz: bd7135922d96fa7789cc65dbd47635c4b1e84a4e7b27a6b4dde5a1a00cc15ac8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0c1e66690ca97e6f9a9349f1960f92f5376175660bebc9a172eabe849af6806d4c5f278f1765a22b36ce75797a757bf9cb3719a58446aaa5fa8fe0356c348cf0
|
7
|
+
data.tar.gz: 35a3e86f1f3c63ec38c3d67adbfb952fa89a062dad78450bfd365f45c1a1607f689e104e2c7ef6faac3a00461e07022d041e5d2f667396ad81baa3af30be8acf
|
data/lib/wafris/configuration.rb
CHANGED
@@ -1,56 +1,140 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
1
|
|
3
2
|
require_relative 'version'
|
4
3
|
|
5
4
|
module Wafris
|
6
5
|
class Configuration
|
7
|
-
|
8
|
-
attr_accessor :
|
9
|
-
attr_accessor :
|
10
|
-
attr_accessor :
|
6
|
+
|
7
|
+
attr_accessor :api_key
|
8
|
+
attr_accessor :db_file_path
|
9
|
+
attr_accessor :db_file_name
|
10
|
+
attr_accessor :downsync_custom_rules_interval
|
11
|
+
attr_accessor :downsync_data_subscriptions_interval
|
12
|
+
attr_accessor :downsync_url
|
13
|
+
attr_accessor :upsync_url
|
14
|
+
attr_accessor :upsync_interval
|
15
|
+
attr_accessor :upsync_queue_limit
|
16
|
+
attr_accessor :upsync_status
|
17
|
+
attr_accessor :upsync_queue
|
18
|
+
attr_accessor :local_only
|
19
|
+
attr_accessor :last_upsync_timestamp
|
20
|
+
attr_accessor :max_body_size_mb
|
21
|
+
attr_accessor :rate_limiters
|
11
22
|
|
12
23
|
def initialize
|
13
|
-
@redis_pool_size = 20
|
14
|
-
@maxmemory = 25
|
15
|
-
@quiet_mode = false
|
16
|
-
end
|
17
24
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
25
|
+
# API Key - Required
|
26
|
+
if ENV['WAFRIS_API_KEY']
|
27
|
+
@api_key = ENV['WAFRIS_API_KEY']
|
28
|
+
else
|
29
|
+
unless @api_key
|
30
|
+
LogSuppressor.puts_log("Firewall disabled as neither local only or API key set")
|
31
|
+
end
|
32
|
+
end
|
22
33
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
) unless @quiet_mode
|
31
|
-
end
|
34
|
+
# DB FILE PATH LOCATION - Optional
|
35
|
+
if ENV['WAFRIS_DB_FILE_PATH']
|
36
|
+
@db_file_path = ENV['WAFRIS_DB_FILE_PATH']
|
37
|
+
else
|
38
|
+
#@db_file_path = Rails.root.join('tmp', 'wafris').to_s
|
39
|
+
@db_file_path = './tmp/wafris'
|
40
|
+
end
|
32
41
|
|
33
|
-
|
34
|
-
|
35
|
-
|
42
|
+
# Ensure that the db_file_path exists
|
43
|
+
unless File.directory?(@db_file_path)
|
44
|
+
LogSuppressor.puts_log("DB File Path does not exist - creating it now.")
|
45
|
+
FileUtils.mkdir_p(@db_file_path) unless File.exist?(@db_file_path)
|
46
|
+
end
|
47
|
+
|
48
|
+
# DB FILE NAME - For local
|
49
|
+
if ENV['WAFRIS_DB_FILE_NAME']
|
50
|
+
@db_file_name = ENV['WAFRIS_DB_FILE_NAME']
|
51
|
+
else
|
52
|
+
@db_file_name = 'wafris.db'
|
53
|
+
end
|
54
|
+
|
55
|
+
# DOWNSYNC
|
56
|
+
# Custom Rules are checked often (default 1 minute) - Optional
|
57
|
+
if ENV['WAFRIS_DOWNSYNC_CUSTOM_RULES_INTERVAL']
|
58
|
+
@downsync_custom_rules_interval = ENV['WAFRIS_DOWNSYNC_CUSTOM_RULES_INTERVAL'].to_i
|
59
|
+
else
|
60
|
+
@downsync_custom_rules_interval = 60
|
61
|
+
end
|
62
|
+
|
63
|
+
# Data Subscriptions are checked rarely (default 1 day) - Optional
|
64
|
+
if ENV['WAFRIS_DOWNSYNC_DATA_SUBSCRIPTIONS_INTERVAL']
|
65
|
+
@downsync_data_subscriptions_interval = ENV['WAFRIS_DOWNSYNC_DATA_SUBSCRIPTIONS_INTERVAL'].to_i
|
66
|
+
else
|
67
|
+
@downsync_data_subscriptions_interval = 60
|
68
|
+
end
|
69
|
+
|
70
|
+
# Set Downsync URL - Optional
|
71
|
+
# Used for both DataSubscription and CustomRules
|
72
|
+
if ENV['WAFRIS_DOWNSYNC_URL']
|
73
|
+
@downsync_url = ENV['WAFRIS_DOWNSYNC_URL']
|
74
|
+
else
|
75
|
+
@downsync_url = 'https://distributor.wafris.org/v2/downsync'
|
76
|
+
end
|
77
|
+
|
78
|
+
# UPSYNC - Optional
|
79
|
+
# Set Upsync URL
|
80
|
+
if ENV['WAFRIS_UPSYNC_URL']
|
81
|
+
@upsync_url = ENV['WAFRIS_UPSYNC_URL']
|
82
|
+
else
|
83
|
+
@upsync_url = 'https://collector.wafris.org/v2/upsync'
|
84
|
+
end
|
85
|
+
|
86
|
+
# Set Upsync Interval - Optional
|
87
|
+
if ENV['WAFRIS_UPSYNC_INTERVAL']
|
88
|
+
@upsync_interval = ENV['WAFRIS_UPSYNC_INTERVAL'].to_i
|
89
|
+
else
|
90
|
+
@upsync_interval = 10
|
91
|
+
end
|
92
|
+
|
93
|
+
# Set Upsync Queued Request Limit - Optional
|
94
|
+
if ENV['WAFRIS_UPSYNC_QUEUE_LIMIT']
|
95
|
+
@upsync_queue_limit = ENV['WAFRIS_UPSYNC_QUEUE_LIMIT'].to_i
|
96
|
+
else
|
97
|
+
@upsync_queue_limit = 250
|
98
|
+
end
|
99
|
+
|
100
|
+
# Set Maximium Body Size for Requests - Optional (in Megabytes)
|
101
|
+
if ENV['WAFRIS_MAX_BODY_SIZE_MB'] && ENV['WAFRIS_MAX_BODY_SIZE_MB'].to_i > 0
|
102
|
+
@max_body_size_mb = ENV['WAFRIS_MAX_BODY_SIZE_MB'].to_i
|
103
|
+
else
|
104
|
+
@max_body_size_mb = 10
|
105
|
+
end
|
36
106
|
|
37
|
-
|
38
|
-
|
107
|
+
# Upsync Queue Defaults
|
108
|
+
@upsync_queue = []
|
109
|
+
@last_upsync_timestamp = Time.now.to_i
|
110
|
+
|
111
|
+
# Memory structure for rate limiting
|
112
|
+
@rate_limiters = {}
|
113
|
+
|
114
|
+
# Disable Upsync if Downsync API Key is invalid
|
115
|
+
# This prevents the client from sending upsync requests
|
116
|
+
# if the API key is known bad
|
117
|
+
@upsync_status = 'Disabled'
|
118
|
+
|
119
|
+
return true
|
120
|
+
|
39
121
|
end
|
40
122
|
|
41
|
-
|
123
|
+
def current_config
|
124
|
+
|
125
|
+
output = {}
|
126
|
+
|
127
|
+
instance_variables.each do |var|
|
128
|
+
output[var.to_s] = instance_variable_get(var)
|
129
|
+
end
|
130
|
+
|
131
|
+
return output
|
42
132
|
|
43
|
-
def read_lua_dist(filename)
|
44
|
-
File.read(
|
45
|
-
file_path(filename)
|
46
|
-
)
|
47
133
|
end
|
48
134
|
|
49
|
-
def
|
50
|
-
|
51
|
-
File.dirname(__FILE__),
|
52
|
-
"../lua/dist/#{filename}.lua"
|
53
|
-
)
|
135
|
+
def create_settings
|
136
|
+
@version = Wafris::VERSION
|
54
137
|
end
|
138
|
+
|
55
139
|
end
|
56
140
|
end
|
@@ -3,11 +3,12 @@
|
|
3
3
|
module Wafris
|
4
4
|
class LogSuppressor
|
5
5
|
def self.puts_log(message)
|
6
|
-
puts(message) unless suppress_logs?
|
6
|
+
puts("[Wafris] " + message) unless suppress_logs?
|
7
7
|
end
|
8
8
|
|
9
9
|
def self.suppress_logs?
|
10
|
-
suppressed_environments.include?(current_environment)
|
10
|
+
suppressed_environments.include?(current_environment) ||
|
11
|
+
(ENV['WAFRIS_LOG_LEVEL'] && ENV['WAFRIS_LOG_LEVEL'] == 'silent')
|
11
12
|
end
|
12
13
|
|
13
14
|
def self.suppressed_environments
|
data/lib/wafris/middleware.rb
CHANGED
@@ -1,12 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
|
3
4
|
module Wafris
|
4
5
|
class Middleware
|
5
6
|
def initialize(app)
|
6
7
|
@app = app
|
7
8
|
end
|
8
9
|
|
10
|
+
|
9
11
|
def call(env)
|
12
|
+
|
10
13
|
user_defined_proxies = ENV['TRUSTED_PROXY_RANGES'].split(',') if ENV['TRUSTED_PROXY_RANGES']
|
11
14
|
|
12
15
|
valid_ipv4_octet = /\.(25[0-5]|2[0-4][0-9]|[01]?[0-9]?[0-9])/
|
@@ -25,25 +28,53 @@ module Wafris
|
|
25
28
|
Rack::Request.ip_filter = lambda { |ip| trusted_proxies.match?(ip) }
|
26
29
|
|
27
30
|
request = Rack::Request.new(env)
|
31
|
+
# Forcing UTF-8 encoding on all strings for Sqlite3 compatibility
|
32
|
+
|
33
|
+
# List of possible IP headers in order of priority
|
34
|
+
ip_headers = [
|
35
|
+
'HTTP_X_REAL_IP',
|
36
|
+
'HTTP_X_TRUE_CLIENT_IP',
|
37
|
+
'HTTP_FLY_CLIENT_IP',
|
38
|
+
'HTTP_CF_CONNECTING_IP'
|
39
|
+
]
|
40
|
+
|
41
|
+
# Find the first header that is present in the environment
|
42
|
+
ip_header = ip_headers.find { |header| env[header] }
|
43
|
+
|
44
|
+
# Use the found header or fallback to remote_ip if none of the headers are present
|
45
|
+
ip = (ip_header ? env[ip_header] : request.ip).force_encoding('UTF-8')
|
46
|
+
|
47
|
+
user_agent = request.user_agent ? request.user_agent.force_encoding('UTF-8') : nil
|
48
|
+
path = request.path.force_encoding('UTF-8')
|
49
|
+
parameters = Rack::Utils.build_query(request.params).force_encoding('UTF-8')
|
50
|
+
host = request.host.to_s.force_encoding('UTF-8')
|
51
|
+
request_method = String.new(request.request_method).force_encoding('UTF-8')
|
52
|
+
|
53
|
+
# Submitted for evaluation
|
54
|
+
headers = env.each_with_object({}) { |(k, v), h| h[k] = v.force_encoding('UTF-8') if k.start_with?('HTTP_') }
|
55
|
+
body = request.body.read
|
56
|
+
|
57
|
+
request_id = env.fetch('action_dispatch.request_id', SecureRandom.uuid.to_s)
|
58
|
+
request_timestamp = Time.now.utc.to_i
|
28
59
|
|
29
|
-
|
30
|
-
|
60
|
+
treatment = Wafris.evaluate(ip, user_agent, path, parameters, host, request_method, headers, body, request_id, request_timestamp)
|
61
|
+
|
62
|
+
# These values match what the client tests expect (200, 404, 403, 500
|
63
|
+
if treatment == 'Allowed' || treatment == 'Passed'
|
64
|
+
@app.call(env)
|
65
|
+
elsif treatment == 'Blocked'
|
66
|
+
[403, { 'content-type' => 'text/plain' }, ['Blocked']]
|
31
67
|
else
|
32
|
-
|
33
|
-
|
34
|
-
)
|
35
|
-
[403, {}, ['Blocked']]
|
68
|
+
#ap request
|
69
|
+
[500, { 'content-type' => 'text/plain' }, ['Error']]
|
36
70
|
end
|
37
|
-
|
38
|
-
LogSuppressor.puts_log(
|
39
|
-
"[Wafris] Wafris timed out during processing. Request passed without rules check."
|
40
|
-
)
|
41
|
-
@app.call(env)
|
71
|
+
|
42
72
|
rescue StandardError => e
|
43
|
-
|
44
|
-
|
45
|
-
)
|
73
|
+
|
74
|
+
LogSuppressor.puts_log "[Wafris] Detailed Error: #{e.class} - #{e.message}"
|
75
|
+
LogSuppressor.puts_log "[Wafris] Backtrace: #{e.backtrace.join("\n")}"
|
46
76
|
@app.call(env)
|
77
|
+
|
47
78
|
end
|
48
79
|
end
|
49
80
|
end
|
data/lib/wafris/version.rb
CHANGED
data/lib/wafris.rb
CHANGED
@@ -1,61 +1,605 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
|
4
4
|
require 'rails'
|
5
|
-
require '
|
5
|
+
require 'sqlite3'
|
6
|
+
require 'ipaddr'
|
7
|
+
require 'httparty'
|
8
|
+
require 'awesome_print'
|
6
9
|
|
7
10
|
require 'wafris/configuration'
|
8
11
|
require 'wafris/middleware'
|
9
12
|
require 'wafris/log_suppressor'
|
10
13
|
|
11
14
|
require 'wafris/railtie' if defined?(Rails::Railtie)
|
15
|
+
ActiveSupport::Deprecation.behavior = :silence
|
12
16
|
|
13
17
|
module Wafris
|
14
18
|
class << self
|
15
19
|
attr_accessor :configuration
|
16
20
|
|
17
21
|
def configure
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
"[Wafris] block is required to configure Wafris. More info can be found at: https://github.com/Wafris/wafris-rb"
|
29
|
-
)
|
30
|
-
rescue StandardError => e
|
31
|
-
LogSuppressor.puts_log(
|
32
|
-
"[Wafris] firewall disabled due to: #{e.message}. Cannot connect via Wafris.configure. Please check your configuration settings. More info can be found at: https://github.com/Wafris/wafris-rb"
|
33
|
-
)
|
34
|
-
end
|
35
|
-
|
36
|
-
def allow_request?(request)
|
37
|
-
configuration.connection_pool.with do |conn|
|
38
|
-
time = Time.now.utc.to_i * 1000
|
39
|
-
status = conn.evalsha(
|
40
|
-
configuration.core_sha,
|
41
|
-
argv: [
|
42
|
-
request.ip,
|
43
|
-
IPAddr.new(request.ip).to_i,
|
44
|
-
time,
|
45
|
-
request.user_agent,
|
46
|
-
request.path,
|
47
|
-
request.query_string,
|
48
|
-
request.host,
|
49
|
-
request.request_method
|
50
|
-
]
|
51
|
-
)
|
22
|
+
begin
|
23
|
+
self.configuration ||= Wafris::Configuration.new
|
24
|
+
yield(configuration)
|
25
|
+
|
26
|
+
LogSuppressor.puts_log("[Wafris] Configuration settings created.")
|
27
|
+
configuration.create_settings
|
28
|
+
|
29
|
+
rescue StandardError => e
|
30
|
+
puts "[Wafris] firewall disabled due to: #{e.message}. Cannot connect via Wafris.configure. Please check your configuration settings. More info can be found at: https://github.com/Wafris/wafris-rb"
|
31
|
+
end
|
52
32
|
|
53
|
-
|
33
|
+
end
|
34
|
+
|
35
|
+
def zero_pad(number, length)
|
36
|
+
number.to_s.rjust(length, "0")
|
37
|
+
end
|
38
|
+
|
39
|
+
def ip_to_decimal_lexical_string(ip)
|
40
|
+
num = 0
|
41
|
+
|
42
|
+
if ip.include?(":")
|
43
|
+
ip = IPAddr.new(ip).to_string
|
44
|
+
hex = ip.delete(":")
|
45
|
+
(0...hex.length).step(4) do |i|
|
46
|
+
chunk = hex[i, 4].to_i(16)
|
47
|
+
num = num * (2**16) + chunk
|
48
|
+
end
|
49
|
+
elsif ip.include?(".")
|
50
|
+
ip.split(".").each do |chunk|
|
51
|
+
num = num * 256 + chunk.to_i
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
str = num.to_s
|
56
|
+
zero_pad(str, 39)
|
57
|
+
end
|
58
|
+
|
59
|
+
def ip_in_cidr_range(ip_address, table_name, db_connection)
|
60
|
+
lexical_address = ip_to_decimal_lexical_string(ip_address)
|
61
|
+
higher_value = db_connection.get_first_value("SELECT * FROM #{table_name} WHERE member > ? ORDER BY member ASC", [lexical_address])
|
62
|
+
lower_value = db_connection.get_first_value("SELECT * FROM #{table_name} WHERE member < ? ORDER BY member DESC", [lexical_address])
|
63
|
+
|
64
|
+
if higher_value.nil? || lower_value.nil?
|
65
|
+
return nil
|
66
|
+
else
|
67
|
+
higher_compare = higher_value.split("-").last
|
68
|
+
lower_compare = lower_value.split("-").last
|
69
|
+
|
70
|
+
if higher_compare == lower_compare
|
71
|
+
return lower_compare
|
72
|
+
else
|
73
|
+
return nil
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def get_country_code(ip, db_connection)
|
79
|
+
country_code = ip_in_cidr_range(ip, 'country_ip_ranges', db_connection)
|
80
|
+
|
81
|
+
if country_code
|
82
|
+
country_code = country_code.split("_").first.split("G").last
|
83
|
+
return country_code
|
84
|
+
else
|
85
|
+
return "ZZ"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def substring_match(request_property, table_name, db_connection)
|
90
|
+
result = db_connection.execute("SELECT entries FROM #{table_name}")
|
91
|
+
result.flatten.each do |entry|
|
92
|
+
if request_property.include?(entry)
|
93
|
+
return entry
|
94
|
+
end
|
95
|
+
end
|
96
|
+
return false
|
97
|
+
end
|
98
|
+
|
99
|
+
def exact_match(request_property, table_name, db_connection)
|
100
|
+
result = db_connection.execute("SELECT entries FROM #{table_name} WHERE entries = ?", [request_property])
|
101
|
+
return result.any?
|
102
|
+
end
|
103
|
+
|
104
|
+
def check_rate_limit(ip, path, method, db_connection)
|
105
|
+
|
106
|
+
# Correctly format the SQL query with placeholders
|
107
|
+
limiters = db_connection.execute("SELECT * FROM blocked_rate_limits WHERE path = ? AND method = ?", [path, method])
|
108
|
+
|
109
|
+
# If no rate limiters are matched
|
110
|
+
if limiters.empty?
|
111
|
+
return false
|
112
|
+
end
|
113
|
+
|
114
|
+
current_timestamp = Time.now.to_i
|
115
|
+
|
116
|
+
# If any rate limiters are matched
|
117
|
+
# This implementation will block the request on any of the rate limiters
|
118
|
+
limiters.each do |limiter|
|
119
|
+
|
120
|
+
# Limiter array mapping
|
121
|
+
# 0: id
|
122
|
+
# 1: path
|
123
|
+
# 2: method
|
124
|
+
# 3: interval
|
125
|
+
# 4: max_count
|
126
|
+
# 5: rule_id
|
127
|
+
|
128
|
+
interval = limiter[3]
|
129
|
+
max_count = limiter[4]
|
130
|
+
rule_id = limiter[5]
|
131
|
+
|
132
|
+
# Expire old timestamps
|
133
|
+
@configuration.rate_limiters.each do |ip, timestamps|
|
134
|
+
# Removes timestamps older than the interval
|
135
|
+
|
136
|
+
@configuration.rate_limiters[ip] = timestamps.select { |timestamp| timestamp > current_timestamp - interval }
|
137
|
+
|
138
|
+
# Remove the IP if there are no more timestamps for the IP
|
139
|
+
@configuration.rate_limiters.delete(ip) if @configuration.rate_limiters[ip].empty?
|
140
|
+
end
|
141
|
+
|
142
|
+
# Check if the IP+Method is rate limited
|
143
|
+
|
144
|
+
if @configuration.rate_limiters[ip] && @configuration.rate_limiters[ip].length >= max_count
|
145
|
+
# Request is rate limited
|
146
|
+
|
147
|
+
|
148
|
+
return rule_id
|
149
|
+
|
150
|
+
else
|
151
|
+
# Request is not rate limited, so add the current timestamp
|
152
|
+
if @configuration.rate_limiters[ip]
|
153
|
+
@configuration.rate_limiters[ip] << current_timestamp
|
154
|
+
else
|
155
|
+
@configuration.rate_limiters[ip] = [current_timestamp]
|
156
|
+
end
|
157
|
+
|
54
158
|
return false
|
159
|
+
end
|
160
|
+
|
161
|
+
end
|
162
|
+
|
163
|
+
end
|
164
|
+
|
165
|
+
def send_upsync_requests(requests_array)
|
166
|
+
|
167
|
+
begin
|
168
|
+
|
169
|
+
headers = {'Content-Type' => 'application/json'}
|
170
|
+
|
171
|
+
if Rails && Rails.application
|
172
|
+
framework = "Rails v#{Rails::VERSION::STRING}"
|
173
|
+
else
|
174
|
+
framework = "Rack v#{Rack::VERSION::STRING}"
|
175
|
+
end
|
176
|
+
|
177
|
+
body = {
|
178
|
+
meta: {
|
179
|
+
version: Wafris::VERSION,
|
180
|
+
client: 'wafris-rb',
|
181
|
+
framework: framework
|
182
|
+
},
|
183
|
+
batch: requests_array
|
184
|
+
}.to_json
|
185
|
+
|
186
|
+
url_and_api_key = @configuration.upsync_url + '/' + @configuration.api_key
|
187
|
+
|
188
|
+
response = HTTParty.post(url_and_api_key,
|
189
|
+
:body => body,
|
190
|
+
:headers => headers,
|
191
|
+
:timeout => 300)
|
192
|
+
|
193
|
+
if response.code == 200
|
194
|
+
@configuration.upsync_status = 'Complete'
|
195
|
+
else
|
196
|
+
LogSuppressor.puts_log("Upsync Error. HTTP Response: #{response.code}")
|
197
|
+
end
|
198
|
+
rescue HTTParty::Error => e
|
199
|
+
LogSuppressor.puts_log("Upsync Error. Failed to send upsync requests: #{e.message}")
|
200
|
+
end
|
201
|
+
return true
|
202
|
+
end
|
203
|
+
|
204
|
+
# This method is used to queue upsync requests. It takes in several parameters including
|
205
|
+
# ip, user_agent, path, parameters, host, method, treatment, category, and rule.
|
206
|
+
#
|
207
|
+
# The 'treatment' parameter represents the action taken on the request, which can be
|
208
|
+
# 'Allowed', 'Blocked', or 'Passed'.
|
209
|
+
#
|
210
|
+
# The 'category' parameter represents the category of the rule that was matched, such as
|
211
|
+
# 'blocked_ip', 'allowed_cidr', etc.
|
212
|
+
#
|
213
|
+
# The 'rule' parameter represents the specific rule that was matched within the category
|
214
|
+
# ex: '192.23.5.4', 'SemRush', etc.
|
215
|
+
def queue_upsync_request(ip, user_agent, path, parameters, host, method, treatment, category, rule, request_id, request_timestamp)
|
216
|
+
|
217
|
+
if @configuration.upsync_status != 'Disabled' || @configuration.upsync_status != 'Uploading'
|
218
|
+
@configuration.upsync_status = 'Uploading'
|
219
|
+
|
220
|
+
# Add request to the queue
|
221
|
+
request = [ip, user_agent, path, parameters, host, method, treatment, category, rule, request_id, request_timestamp]
|
222
|
+
@configuration.upsync_queue << request
|
223
|
+
|
224
|
+
# If the queue is full, send the requests to the upsync server
|
225
|
+
if @configuration.upsync_queue.length >= @configuration.upsync_queue_limit || (Time.now.to_i - @configuration.last_upsync_timestamp) >= @configuration.upsync_interval
|
226
|
+
requests_array = @configuration.upsync_queue
|
227
|
+
@configuration.upsync_queue = []
|
228
|
+
@configuration.last_upsync_timestamp = Time.now.to_i
|
229
|
+
|
230
|
+
send_upsync_requests(requests_array)
|
231
|
+
end
|
232
|
+
|
233
|
+
@configuration.upsync_status = 'Enabled'
|
234
|
+
# Return the treatment - used to return 403 or 200
|
235
|
+
|
236
|
+
message = "Request #{treatment}"
|
237
|
+
message += " | Category: #{category}" unless category.blank?
|
238
|
+
message += " | Rule: #{rule}" unless rule.blank?
|
239
|
+
LogSuppressor.puts_log(message)
|
240
|
+
|
241
|
+
return treatment
|
242
|
+
else
|
243
|
+
@configuration.upsync_status = 'Enabled'
|
244
|
+
return "Passed"
|
245
|
+
end
|
246
|
+
|
247
|
+
end
|
248
|
+
|
249
|
+
# Pulls the latest rules from the server
|
250
|
+
def downsync_db(db_rule_category, current_filename = nil)
|
251
|
+
|
252
|
+
lockfile_path = "#{@configuration.db_file_path}/#{db_rule_category}.lockfile"
|
253
|
+
|
254
|
+
# Ensure the directory exists before attempting to open the lockfile
|
255
|
+
FileUtils.mkdir_p(@configuration.db_file_path) unless Dir.exist?(@configuration.db_file_path)
|
256
|
+
|
257
|
+
# Attempt to create a lockfile with exclusive access; skip if it exists
|
258
|
+
begin
|
259
|
+
lockfile = File.open(lockfile_path, File::RDWR|File::CREAT|File::EXCL)
|
260
|
+
rescue Errno::EEXIST
|
261
|
+
LogSuppressor.puts_log("[Wafris][Downsync] Lockfile already exists, skipping downsync.")
|
262
|
+
return
|
263
|
+
rescue Exception => e
|
264
|
+
LogSuppressor.puts_log("[Wafris][Downsync] Error creating lockfile: #{e.message}")
|
265
|
+
end
|
266
|
+
|
267
|
+
begin
|
268
|
+
# Actual Downsync operations
|
269
|
+
filename = ""
|
270
|
+
|
271
|
+
if Rails && Rails.application
|
272
|
+
framework = "Rails v#{Rails::VERSION::STRING}"
|
55
273
|
else
|
56
|
-
|
274
|
+
framework = "Rack v#{Rack::VERSION::STRING}"
|
57
275
|
end
|
276
|
+
|
277
|
+
data = {
|
278
|
+
client_db: current_filename,
|
279
|
+
process_id: Process.pid,
|
280
|
+
hostname: Socket.gethostname,
|
281
|
+
version: Wafris::VERSION,
|
282
|
+
client: 'wafris-rb',
|
283
|
+
framework: framework
|
284
|
+
}
|
285
|
+
|
286
|
+
# Check server for new rules including process id
|
287
|
+
#puts "Downloading from #{@configuration.downsync_url}/#{db_rule_category}/#{@configuration.api_key}?current_version=#{current_filename}&process_id=#{Process.pid}"
|
288
|
+
uri = "#{@configuration.downsync_url}/#{db_rule_category}/#{@configuration.api_key}?#{data.to_query}"
|
289
|
+
|
290
|
+
response = HTTParty.get(
|
291
|
+
uri,
|
292
|
+
follow_redirects: true, # Enable following redirects
|
293
|
+
max_redirects: 2 # Maximum number of redirects to follow
|
294
|
+
)
|
295
|
+
|
296
|
+
# TODO: What to do if timeout
|
297
|
+
# TODO: What to do if error
|
298
|
+
|
299
|
+
if response.code == 401
|
300
|
+
@configuration.upsync_status = 'Disabled'
|
301
|
+
LogSuppressor.puts_log("[Wafris][Downsync] Unauthorized: Bad or missing API key")
|
302
|
+
LogSuppressor.puts_log("[Wafris][Downsync] API Key: #{@configuration.api_key}")
|
303
|
+
filename = current_filename
|
304
|
+
|
305
|
+
elsif response.code == 304
|
306
|
+
@configuration.upsync_status = 'Enabled'
|
307
|
+
LogSuppressor.puts_log("[Wafris][Downsync] No new rules to download")
|
308
|
+
|
309
|
+
filename = current_filename
|
310
|
+
|
311
|
+
elsif response.code == 200
|
312
|
+
@configuration.upsync_status = 'Enabled'
|
313
|
+
|
314
|
+
if current_filename
|
315
|
+
old_file_name = current_filename
|
316
|
+
end
|
317
|
+
|
318
|
+
# Extract the filename from the response
|
319
|
+
content_disposition = response.headers['content-disposition']
|
320
|
+
filename = content_disposition.split('filename=')[1].strip
|
321
|
+
|
322
|
+
# Save the body of the response to a new SQLite file
|
323
|
+
File.open(@configuration.db_file_path + "/" + filename, 'wb') { |file| file.write(response.body) }
|
324
|
+
|
325
|
+
# Write the filename into the db_category.modfile
|
326
|
+
File.open("#{@configuration.db_file_path}/#{db_rule_category}.modfile", 'w') { |file| file.write(filename) }
|
327
|
+
|
328
|
+
# Sanity check that the downloaded db file has tables
|
329
|
+
# not empty or corrupted
|
330
|
+
db = SQLite3::Database.new @configuration.db_file_path + "/" + filename
|
331
|
+
if db.execute("SELECT name FROM sqlite_master WHERE type='table';").any?
|
332
|
+
# Remove the old database file
|
333
|
+
if old_file_name
|
334
|
+
if File.exist?(@configuration.db_file_path + "/" + old_file_name)
|
335
|
+
File.delete(@configuration.db_file_path + "/" + old_file_name)
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
# DB file is bad or empty so keep using whatever we have now
|
340
|
+
else
|
341
|
+
filename = old_file_name
|
342
|
+
LogSuppressor.puts_log("[Wafris][Downsync] DB Error - No tables exist in the db file #{@configuration.db_file_path}/#{filename}")
|
343
|
+
end
|
344
|
+
|
345
|
+
|
346
|
+
end
|
347
|
+
|
348
|
+
rescue Exception => e
|
349
|
+
LogSuppressor.puts_log("[Wafris][Downsync] Error downloading rules: #{e.message}")
|
350
|
+
|
351
|
+
# This gets set even if the API key is bad or other issues
|
352
|
+
# to prevent hammering the distribution server on every request
|
353
|
+
ensure
|
354
|
+
|
355
|
+
# Reset the modified time of the modfile
|
356
|
+
unless File.exist?("#{@configuration.db_file_path}/#{db_rule_category}.modfile")
|
357
|
+
File.new("#{@configuration.db_file_path}/#{db_rule_category}.modfile", 'w')
|
358
|
+
end
|
359
|
+
|
360
|
+
# Set the modified time of the modfile to the current time
|
361
|
+
File.utime(Time.now, Time.now, "#{@configuration.db_file_path}/#{db_rule_category}.modfile")
|
362
|
+
|
363
|
+
# Ensure the lockfile is removed after operations
|
364
|
+
lockfile.close
|
365
|
+
File.delete(lockfile_path)
|
366
|
+
end
|
367
|
+
|
368
|
+
return filename
|
369
|
+
|
370
|
+
end
|
371
|
+
|
372
|
+
# Returns the current database file,
|
373
|
+
# if the file is older than the interval, it will download the latest db
|
374
|
+
# if the file doesn't exist, it will download the latest db
|
375
|
+
# if the lockfile exists, it will return the current db
|
376
|
+
def current_db(db_rule_category)
|
377
|
+
|
378
|
+
if db_rule_category == 'custom_rules'
|
379
|
+
interval = @configuration.downsync_custom_rules_interval
|
380
|
+
else
|
381
|
+
interval = @configuration.downsync_data_subscriptions_interval
|
382
|
+
end
|
383
|
+
|
384
|
+
# Checks for existing current modfile, which contains the current db filename
|
385
|
+
if File.exist?("#{@configuration.db_file_path}/#{db_rule_category}.modfile")
|
386
|
+
|
387
|
+
LogSuppressor.puts_log("[Wafris][Downsync] Modfile exists, skipping downsync")
|
388
|
+
|
389
|
+
# Get last Modified Time and current database file name
|
390
|
+
last_db_synctime = File.mtime("#{@configuration.db_file_path}/#{db_rule_category}.modfile").to_i
|
391
|
+
returned_db = File.read("#{@configuration.db_file_path}/#{db_rule_category}.modfile").strip
|
392
|
+
|
393
|
+
LogSuppressor.puts_log("[Wafris][Downsync] Modfile Last Modified Time: #{last_db_synctime}")
|
394
|
+
LogSuppressor.puts_log("[Wafris][Downsync] DB in Modfile: #{returned_db}")
|
395
|
+
|
396
|
+
# Check if the db file is older than the interval
|
397
|
+
if (Time.now.to_i - last_db_synctime) > interval
|
398
|
+
|
399
|
+
LogSuppressor.puts_log("[Wafris][Downsync] DB is older than the interval")
|
400
|
+
|
401
|
+
# Make sure that another process isn't already downloading the rules
|
402
|
+
if !File.exist?("#{@configuration.db_file_path}/#{db_rule_category}.lockfile")
|
403
|
+
returned_db = downsync_db(db_rule_category, returned_db)
|
404
|
+
end
|
405
|
+
|
406
|
+
return returned_db
|
407
|
+
|
408
|
+
# Current db is up to date
|
409
|
+
else
|
410
|
+
|
411
|
+
LogSuppressor.puts_log("[Wafris][Downsync] DB is up to date")
|
412
|
+
|
413
|
+
returned_db = File.read("#{@configuration.db_file_path}/#{db_rule_category}.modfile").strip
|
414
|
+
|
415
|
+
# If the modfile is empty (no db file name), return nil
|
416
|
+
# this can happen if the the api key is bad
|
417
|
+
if returned_db == ''
|
418
|
+
return ''
|
419
|
+
else
|
420
|
+
return returned_db
|
421
|
+
end
|
422
|
+
|
423
|
+
end
|
424
|
+
|
425
|
+
# No modfile exists, so download the latest db
|
426
|
+
else
|
427
|
+
|
428
|
+
LogSuppressor.puts_log("[Wafris][Downsync] No modfile exists, downloading latest db")
|
429
|
+
|
430
|
+
# Make sure that another process isn't already downloading the rules
|
431
|
+
if File.exist?("#{@configuration.db_file_path}/#{db_rule_category}.lockfile")
|
432
|
+
LogSuppressor.puts_log("[Wafris][Downsync] Lockfile exists, skipping downsync")
|
433
|
+
# Lockfile exists, but no modfile with a db filename
|
434
|
+
return nil
|
435
|
+
else
|
436
|
+
|
437
|
+
LogSuppressor.puts_log("[Wafris][Downsync] No modfile exists, downloading latest db")
|
438
|
+
# No modfile exists, so download the latest db
|
439
|
+
returned_db = downsync_db(db_rule_category, nil)
|
440
|
+
|
441
|
+
if returned_db.nil?
|
442
|
+
return nil
|
443
|
+
else
|
444
|
+
return returned_db
|
445
|
+
end
|
446
|
+
|
447
|
+
end
|
448
|
+
|
449
|
+
end
|
450
|
+
|
451
|
+
end
|
452
|
+
|
453
|
+
# This is the main loop that evaluates the request
|
454
|
+
# as well as sorts out when downsync and upsync should be called
|
455
|
+
def evaluate(ip, user_agent, path, parameters, host, method, headers, body, request_id, request_timestamp)
|
456
|
+
@configuration ||= Wafris::Configuration.new
|
457
|
+
|
458
|
+
if @configuration.api_key.nil?
|
459
|
+
return "Passed"
|
460
|
+
else
|
461
|
+
|
462
|
+
rules_db_filename = current_db('custom_rules')
|
463
|
+
data_subscriptions_db_filename = current_db('data_subscriptions')
|
464
|
+
|
465
|
+
if rules_db_filename.to_s.strip != '' && data_subscriptions_db_filename.strip.to_s.strip != ''
|
466
|
+
|
467
|
+
rules_db = SQLite3::Database.new "#{@configuration.db_file_path}/#{rules_db_filename}"
|
468
|
+
data_subscriptions_db = SQLite3::Database.new "#{@configuration.db_file_path}/#{data_subscriptions_db_filename}"
|
469
|
+
|
470
|
+
# Allowed IPs
|
471
|
+
if exact_match(ip, 'allowed_ips', rules_db)
|
472
|
+
return queue_upsync_request(ip, user_agent, path, parameters, host, method, 'Allowed', 'ai', ip, request_id, request_timestamp)
|
473
|
+
end
|
474
|
+
|
475
|
+
# Allowed CIDR Ranges
|
476
|
+
if ip_in_cidr_range(ip, 'allowed_cidr_ranges', rules_db)
|
477
|
+
return queue_upsync_request(ip, user_agent, path, parameters, host, method, 'Allowed', 'ac', ip, request_id, request_timestamp)
|
478
|
+
end
|
479
|
+
|
480
|
+
# Blocked IPs
|
481
|
+
if exact_match(ip, 'blocked_ips', rules_db)
|
482
|
+
return queue_upsync_request(ip, user_agent, path, parameters, host, method, 'Blocked', 'bi', ip, request_id, request_timestamp)
|
483
|
+
end
|
484
|
+
|
485
|
+
# Blocked CIDR Ranges
|
486
|
+
if ip_in_cidr_range(ip, 'blocked_cidr_ranges', rules_db)
|
487
|
+
return queue_upsync_request(ip, user_agent, path, parameters, host, method, 'Blocked', 'bc', ip, request_id, request_timestamp)
|
488
|
+
end
|
489
|
+
|
490
|
+
# Blocked Country Codes
|
491
|
+
country_code = get_country_code(ip, data_subscriptions_db)
|
492
|
+
if exact_match(country_code, 'blocked_country_codes', rules_db)
|
493
|
+
return queue_upsync_request(ip, user_agent, path, parameters, host, method, 'Blocked', 'bs', "G_#{country_code}", request_id, request_timestamp)
|
494
|
+
end
|
495
|
+
|
496
|
+
# Blocked Reputation IP Ranges
|
497
|
+
if ip_in_cidr_range(ip, 'reputation_ip_ranges', data_subscriptions_db)
|
498
|
+
return queue_upsync_request(ip, user_agent, path, parameters, host, method, 'Blocked', 'bs', "R", request_id, request_timestamp)
|
499
|
+
end
|
500
|
+
|
501
|
+
# Blocked User Agents
|
502
|
+
user_agent_match = substring_match(user_agent, 'blocked_user_agents', rules_db)
|
503
|
+
if user_agent_match
|
504
|
+
return queue_upsync_request(ip, user_agent, path, parameters, host, method, 'Blocked', 'bu', user_agent_match, request_id, request_timestamp)
|
505
|
+
end
|
506
|
+
|
507
|
+
# Blocked Paths
|
508
|
+
path_match = substring_match(path, 'blocked_paths', rules_db)
|
509
|
+
if path_match
|
510
|
+
return queue_upsync_request(ip, user_agent, path, parameters, host, method, 'Blocked', 'bp', path_match, request_id, request_timestamp)
|
511
|
+
end
|
512
|
+
|
513
|
+
# Blocked Parameters
|
514
|
+
parameters_match = substring_match(parameters, 'blocked_parameters', rules_db)
|
515
|
+
if parameters_match
|
516
|
+
return queue_upsync_request(ip, user_agent, path, parameters, host, method, 'Blocked', 'ba', parameters_match, request_id, request_timestamp)
|
517
|
+
end
|
518
|
+
|
519
|
+
# Blocked Hosts
|
520
|
+
if exact_match(host, 'blocked_hosts', rules_db)
|
521
|
+
return queue_upsync_request(ip, user_agent, path, parameters, host, method, 'Blocked', 'bh', host, request_id, request_timestamp)
|
522
|
+
end
|
523
|
+
|
524
|
+
# Blocked Methods
|
525
|
+
if exact_match(method, 'blocked_methods', rules_db)
|
526
|
+
return queue_upsync_request(ip, user_agent, path, parameters, host, method, 'Blocked', 'bm', method, request_id, request_timestamp)
|
527
|
+
end
|
528
|
+
|
529
|
+
# Rate Limiting
|
530
|
+
rule_id = check_rate_limit(ip, path, method, rules_db)
|
531
|
+
if rule_id
|
532
|
+
return queue_upsync_request(ip, user_agent, path, parameters, host, method, 'Blocked', 'brl', rule_id, request_id, request_timestamp)
|
533
|
+
end
|
534
|
+
|
535
|
+
end
|
536
|
+
|
537
|
+
# Passed if no allow or block rules matched
|
538
|
+
return queue_upsync_request(ip, user_agent, path, parameters, host, method, 'Passed', 'passed', '-', request_id, request_timestamp)
|
539
|
+
|
540
|
+
end # end api_key.nil?
|
541
|
+
end # end evaluate
|
542
|
+
|
543
|
+
def debug(api_key)
|
544
|
+
|
545
|
+
if ENV['WAFRIS_API_KEY']
|
546
|
+
puts "Wafris API Key environment variable is set."
|
547
|
+
puts " - API Key: #{ENV['WAFRIS_API_KEY']}"
|
548
|
+
else
|
549
|
+
puts "Wafris API Key environment variable is not set."
|
58
550
|
end
|
551
|
+
|
552
|
+
puts "\n"
|
553
|
+
puts "Wafris Configuration:"
|
554
|
+
|
555
|
+
Wafris.configure do |config|
|
556
|
+
config.api_key = api_key
|
557
|
+
end
|
558
|
+
|
559
|
+
settings = Wafris.configuration
|
560
|
+
|
561
|
+
settings.instance_variables.each do |ivar|
|
562
|
+
puts " - #{ivar}: #{Wafris.configuration.instance_variable_get(ivar)}"
|
563
|
+
end
|
564
|
+
|
565
|
+
puts "\n"
|
566
|
+
if File.exist?(settings.db_file_path + "/" + "custom_rules.lockfile")
|
567
|
+
puts "Custom Rules Lockfile: #{settings.db_file_path}/custom_rules.lockfile exists"
|
568
|
+
puts " - Last Modified Time: #{File.mtime(settings.db_file_path + "/" + "custom_rules.lockfile")}"
|
569
|
+
else
|
570
|
+
puts "Custom Rules Lockfile: #{settings.db_file_path}/custom_rules.lockfile does not exist."
|
571
|
+
end
|
572
|
+
|
573
|
+
puts "\n"
|
574
|
+
if File.exist?(settings.db_file_path + "/" + "custom_rules.modfile")
|
575
|
+
puts "Custom Rules Modfile: #{settings.db_file_path}/custom_rules.modfile exists"
|
576
|
+
puts " - Last Modified Time: #{File.mtime(settings.db_file_path + "/" + "custom_rules.modfile")}"
|
577
|
+
puts " - Contents: #{File.open(settings.db_file_path + "/" + "custom_rules.modfile", 'r').read}"
|
578
|
+
else
|
579
|
+
puts "Custom Rules Modfile: #{settings.db_file_path}/custom_rules.modfile does not exist."
|
580
|
+
end
|
581
|
+
|
582
|
+
puts "\n"
|
583
|
+
if File.exist?(settings.db_file_path + "/" + "data_subscriptions.lockfile")
|
584
|
+
puts "Data Subscriptions Lockfile: #{settings.db_file_path}/data_subscriptions.lockfile exists"
|
585
|
+
puts " - Last Modified Time: #{File.mtime(settings.db_file_path + "/" + "data_subscriptions.lockfile")}"
|
586
|
+
else
|
587
|
+
puts "Data Subscriptions Lockfile: #{settings.db_file_path}/data_subscriptions.lockfile does not exist."
|
588
|
+
end
|
589
|
+
|
590
|
+
puts "\n"
|
591
|
+
if File.exist?(settings.db_file_path + "/" + "data_subscriptions.modfile")
|
592
|
+
puts "Data Subscriptions Modfile: #{settings.db_file_path}/data_subscriptions.modfile exists"
|
593
|
+
puts " - Last Modified Time: #{File.mtime(settings.db_file_path + "/" + "data_subscriptions.modfile")}"
|
594
|
+
puts " - Contents: #{File.open(settings.db_file_path + "/" + "data_subscriptions.modfile", 'r').read}"
|
595
|
+
else
|
596
|
+
puts "Data Subscriptions Modfile: #{settings.db_file_path}/data_subscriptions.modfile does not exist."
|
597
|
+
end
|
598
|
+
|
599
|
+
|
600
|
+
|
601
|
+
return true
|
59
602
|
end
|
603
|
+
|
60
604
|
end
|
61
605
|
end
|
metadata
CHANGED
@@ -1,58 +1,86 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: wafris
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
|
-
-
|
7
|
+
- Michael Buckbee
|
8
8
|
- Ryan Castillo
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2024-
|
12
|
+
date: 2024-07-17 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
|
-
name:
|
15
|
+
name: rack
|
16
16
|
requirement: !ruby/object:Gem::Requirement
|
17
17
|
requirements:
|
18
18
|
- - ">="
|
19
19
|
- !ruby/object:Gem::Version
|
20
|
-
version: '2.
|
20
|
+
version: '2.0'
|
21
21
|
type: :runtime
|
22
22
|
prerelease: false
|
23
23
|
version_requirements: !ruby/object:Gem::Requirement
|
24
24
|
requirements:
|
25
25
|
- - ">="
|
26
26
|
- !ruby/object:Gem::Version
|
27
|
-
version: '2.
|
27
|
+
version: '2.0'
|
28
28
|
- !ruby/object:Gem::Dependency
|
29
|
-
name:
|
29
|
+
name: sqlite3
|
30
30
|
requirement: !ruby/object:Gem::Requirement
|
31
31
|
requirements:
|
32
32
|
- - ">="
|
33
33
|
- !ruby/object:Gem::Version
|
34
|
-
version: '
|
34
|
+
version: '0'
|
35
35
|
type: :runtime
|
36
36
|
prerelease: false
|
37
37
|
version_requirements: !ruby/object:Gem::Requirement
|
38
38
|
requirements:
|
39
39
|
- - ">="
|
40
40
|
- !ruby/object:Gem::Version
|
41
|
-
version: '
|
41
|
+
version: '0'
|
42
42
|
- !ruby/object:Gem::Dependency
|
43
|
-
name:
|
43
|
+
name: ipaddr
|
44
44
|
requirement: !ruby/object:Gem::Requirement
|
45
45
|
requirements:
|
46
46
|
- - ">="
|
47
47
|
- !ruby/object:Gem::Version
|
48
|
-
version:
|
48
|
+
version: '0'
|
49
49
|
type: :runtime
|
50
50
|
prerelease: false
|
51
51
|
version_requirements: !ruby/object:Gem::Requirement
|
52
52
|
requirements:
|
53
53
|
- - ">="
|
54
54
|
- !ruby/object:Gem::Version
|
55
|
-
version:
|
55
|
+
version: '0'
|
56
|
+
- !ruby/object:Gem::Dependency
|
57
|
+
name: httparty
|
58
|
+
requirement: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
63
|
+
type: :runtime
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - ">="
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
- !ruby/object:Gem::Dependency
|
71
|
+
name: awesome_print
|
72
|
+
requirement: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - ">="
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '0'
|
77
|
+
type: :runtime
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - ">="
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '0'
|
56
84
|
- !ruby/object:Gem::Dependency
|
57
85
|
name: minitest
|
58
86
|
requirement: !ruby/object:Gem::Requirement
|
@@ -107,14 +135,14 @@ dependencies:
|
|
107
135
|
requirements:
|
108
136
|
- - ">="
|
109
137
|
- !ruby/object:Gem::Version
|
110
|
-
version: '
|
138
|
+
version: '6.0'
|
111
139
|
type: :development
|
112
140
|
prerelease: false
|
113
141
|
version_requirements: !ruby/object:Gem::Requirement
|
114
142
|
requirements:
|
115
143
|
- - ">="
|
116
144
|
- !ruby/object:Gem::Version
|
117
|
-
version: '
|
145
|
+
version: '6.0'
|
118
146
|
- !ruby/object:Gem::Dependency
|
119
147
|
name: railties
|
120
148
|
requirement: !ruby/object:Gem::Requirement
|
@@ -149,7 +177,6 @@ executables: []
|
|
149
177
|
extensions: []
|
150
178
|
extra_rdoc_files: []
|
151
179
|
files:
|
152
|
-
- lib/lua/dist/wafris_core.lua
|
153
180
|
- lib/wafris.rb
|
154
181
|
- lib/wafris/configuration.rb
|
155
182
|
- lib/wafris/log_suppressor.rb
|
@@ -161,11 +188,9 @@ licenses:
|
|
161
188
|
- Elastic-2.0
|
162
189
|
metadata: {}
|
163
190
|
post_install_message: |2+
|
164
|
-
Thank you for installing the
|
165
|
-
|
166
|
-
If you haven't already, please sign up for Wafris Hub at:
|
191
|
+
Thank you for installing the Wafris gem.
|
167
192
|
|
168
|
-
https://
|
193
|
+
Get your API key and set firewall rules at https://hub.wafris.org
|
169
194
|
|
170
195
|
rdoc_options: []
|
171
196
|
require_paths:
|
@@ -1,305 +0,0 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
local USE_TIMESTAMPS_AS_REQUEST_IDS = false
|
4
|
-
local EXPIRATION_IN_SECONDS = tonumber(redis.call("HGET", "waf-settings", "expiration-time")) or 86400
|
5
|
-
local EXPIRATION_OFFSET_IN_SECONDS = 3600
|
6
|
-
|
7
|
-
|
8
|
-
local function get_timebucket(timestamp_in_seconds)
|
9
|
-
local startOfHourTimestamp = math.floor(timestamp_in_seconds / 3600) * 3600
|
10
|
-
return tostring(startOfHourTimestamp)
|
11
|
-
end
|
12
|
-
|
13
|
-
local function set_property_value_id_lookups(property_abbreviation, property_value)
|
14
|
-
|
15
|
-
local value_key = property_abbreviation .. "V" .. property_value
|
16
|
-
local property_id = redis.call("GET", value_key)
|
17
|
-
|
18
|
-
if property_id == false then
|
19
|
-
property_id = redis.call("INCR", property_abbreviation .. "-id-counter")
|
20
|
-
redis.call("SET", value_key, property_id)
|
21
|
-
redis.call("SET", property_abbreviation .. "I" .. property_id, property_value)
|
22
|
-
end
|
23
|
-
|
24
|
-
redis.call("EXPIRE", value_key, EXPIRATION_IN_SECONDS + EXPIRATION_OFFSET_IN_SECONDS)
|
25
|
-
redis.call("EXPIRE", property_abbreviation .. "I" .. property_id, EXPIRATION_IN_SECONDS + EXPIRATION_OFFSET_IN_SECONDS)
|
26
|
-
|
27
|
-
return property_id
|
28
|
-
end
|
29
|
-
|
30
|
-
local function increment_leaderboard_for(property_abbreviation, property_id, timebucket)
|
31
|
-
|
32
|
-
local key = property_abbreviation .. "L" .. timebucket
|
33
|
-
redis.call("ZINCRBY", key, 1, property_id)
|
34
|
-
redis.call("EXPIRE", key, EXPIRATION_IN_SECONDS)
|
35
|
-
end
|
36
|
-
|
37
|
-
local function set_property_to_requests_list(property_abbreviation, property_id, request_id, timebucket)
|
38
|
-
|
39
|
-
local key = property_abbreviation .. "R" .. property_id .. "-" .. timebucket
|
40
|
-
redis.call("LPUSH", key, request_id)
|
41
|
-
|
42
|
-
redis.call("EXPIRE", key, EXPIRATION_IN_SECONDS + EXPIRATION_OFFSET_IN_SECONDS)
|
43
|
-
end
|
44
|
-
|
45
|
-
|
46
|
-
local function ip_in_hash(hash_name, ip_address)
|
47
|
-
local found_ip = redis.call('HEXISTS', hash_name, ip_address)
|
48
|
-
|
49
|
-
if found_ip == 1 then
|
50
|
-
return ip_address
|
51
|
-
else
|
52
|
-
return false
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
|
-
local function ip_in_cidr_range(cidr_set, ip_decimal_lexical)
|
57
|
-
|
58
|
-
local higher_value = redis.call('ZRANGEBYLEX', cidr_set, '['..ip_decimal_lexical, '+', 'LIMIT', 0, 1)[1]
|
59
|
-
|
60
|
-
local lower_value = redis.call('ZREVRANGEBYLEX', cidr_set, '['..ip_decimal_lexical, '-', 'LIMIT', 0, 1)[1]
|
61
|
-
|
62
|
-
if not (higher_value and lower_value) then
|
63
|
-
return false
|
64
|
-
end
|
65
|
-
|
66
|
-
local higher_compare = higher_value:match('([^%-]+)$')
|
67
|
-
local lower_compare = lower_value:match('([^%-]+)$')
|
68
|
-
|
69
|
-
if higher_compare == lower_compare then
|
70
|
-
return lower_compare
|
71
|
-
else
|
72
|
-
return false
|
73
|
-
end
|
74
|
-
end
|
75
|
-
|
76
|
-
local function escapePattern(s)
|
77
|
-
local patternSpecials = "[%^%$%(%)%%%.%[%]%*%+%-%?]"
|
78
|
-
return s:gsub(patternSpecials, "%%%1")
|
79
|
-
end
|
80
|
-
|
81
|
-
local function match_by_pattern(property_abbreviation, property_value)
|
82
|
-
local hash_name = "rules-blocked-" .. property_abbreviation
|
83
|
-
|
84
|
-
local patterns = redis.call('HKEYS', hash_name)
|
85
|
-
|
86
|
-
for _, pattern in ipairs(patterns) do
|
87
|
-
if string.find(string.lower(property_value), string.lower(escapePattern(pattern))) then
|
88
|
-
return pattern
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
|
-
return false
|
93
|
-
end
|
94
|
-
|
95
|
-
local function blocked_by_rate_limit(request_properties)
|
96
|
-
|
97
|
-
local rate_limiting_rules_values = redis.call('HKEYS', 'rules-blocked-rate-limits')
|
98
|
-
|
99
|
-
for i, rule_name in ipairs(rate_limiting_rules_values) do
|
100
|
-
|
101
|
-
local conditions_hash = redis.call('HGETALL', rule_name .. "-conditions")
|
102
|
-
|
103
|
-
local all_conditions_match = true
|
104
|
-
|
105
|
-
for j = 1, #conditions_hash, 2 do
|
106
|
-
local condition_key = conditions_hash[j]
|
107
|
-
local condition_value = conditions_hash[j + 1]
|
108
|
-
|
109
|
-
if request_properties[condition_key] ~= condition_value then
|
110
|
-
all_conditions_match = false
|
111
|
-
break
|
112
|
-
end
|
113
|
-
end
|
114
|
-
|
115
|
-
if all_conditions_match then
|
116
|
-
|
117
|
-
local rule_settings_key = rule_name .. "-settings"
|
118
|
-
|
119
|
-
local limit, time_period, limited_by, rule_id = unpack(redis.call('HMGET', rule_settings_key, 'limit', 'time-period', 'limited-by', 'rule-id'))
|
120
|
-
|
121
|
-
local throttle_key = rule_name .. ":" .. limit .. "V" .. request_properties.ip
|
122
|
-
|
123
|
-
local new_value = redis.call('INCR', throttle_key)
|
124
|
-
|
125
|
-
if new_value == 1 then
|
126
|
-
redis.call('EXPIRE', throttle_key, tonumber(time_period))
|
127
|
-
end
|
128
|
-
|
129
|
-
if tonumber(new_value) >= tonumber(limit) then
|
130
|
-
return rule_id
|
131
|
-
else
|
132
|
-
return false
|
133
|
-
end
|
134
|
-
end
|
135
|
-
end
|
136
|
-
end
|
137
|
-
|
138
|
-
local function check_rules(functions_to_check)
|
139
|
-
for _, check in ipairs(functions_to_check) do
|
140
|
-
|
141
|
-
local rule = check.func(unpack(check.args))
|
142
|
-
local category = check.category
|
143
|
-
|
144
|
-
if type(rule) == "string" then
|
145
|
-
return rule, category
|
146
|
-
end
|
147
|
-
end
|
148
|
-
|
149
|
-
return false, false
|
150
|
-
end
|
151
|
-
|
152
|
-
local function check_blocks(request)
|
153
|
-
local rule_categories = {
|
154
|
-
{ category = "bi", func = ip_in_hash, args = { "rules-blocked-i", request.ip } },
|
155
|
-
{ category = "bc", func = ip_in_cidr_range, args = { "rules-blocked-cidrs-set", request.ip_decimal_lexical } },
|
156
|
-
{ category = "bs", func = ip_in_cidr_range, args = { "rules-blocked-cidrs-subscriptions-set", request.ip_decimal_lexical } },
|
157
|
-
{ category = "bu", func = match_by_pattern, args = { "u", request.user_agent } },
|
158
|
-
{ category = "bp", func = match_by_pattern, args = { "p", request.path } },
|
159
|
-
{ category = "ba", func = match_by_pattern, args = { "a", request.parameters } },
|
160
|
-
{ category = "bh", func = match_by_pattern, args = { "h", request.host } },
|
161
|
-
{ category = "bm", func = match_by_pattern, args = { "m", request.method } },
|
162
|
-
{ category = "bd", func = match_by_pattern, args = { "rh", request.headers } },
|
163
|
-
{ category = "bpb", func = match_by_pattern, args = { "pb", request.post_body } },
|
164
|
-
{ category = "brl", func = blocked_by_rate_limit, args = { request } }
|
165
|
-
}
|
166
|
-
|
167
|
-
return check_rules(rule_categories)
|
168
|
-
end
|
169
|
-
|
170
|
-
local function check_allowed(request)
|
171
|
-
local rule_categories = {
|
172
|
-
{ category = "ai", func = ip_in_hash, args = { "rules-allowed-i", request.ip } },
|
173
|
-
{ category = "ac", func = ip_in_cidr_range, args = { "rules-allowed-cidrs-set", request.ip_decimal_lexical } }
|
174
|
-
}
|
175
|
-
|
176
|
-
return check_rules(rule_categories)
|
177
|
-
end
|
178
|
-
|
179
|
-
local request = {
|
180
|
-
["ip"] = ARGV[1],
|
181
|
-
["ip_decimal_lexical"] = string.rep("0", 39 - #ARGV[2]) .. ARGV[2],
|
182
|
-
["ts_in_milliseconds"] = ARGV[3],
|
183
|
-
["ts_in_seconds"] = ARGV[3] / 1000,
|
184
|
-
["user_agent"] = ARGV[4],
|
185
|
-
["path"] = ARGV[5],
|
186
|
-
["parameters"] = ARGV[6],
|
187
|
-
["host"] = ARGV[7],
|
188
|
-
["method"] = ARGV[8],
|
189
|
-
["headers"] = ARGV[9],
|
190
|
-
["post_body"] = ARGV[10],
|
191
|
-
["ip_id"] = set_property_value_id_lookups("i", ARGV[1]),
|
192
|
-
["user_agent_id"] = set_property_value_id_lookups("u", ARGV[4]),
|
193
|
-
["path_id"] = set_property_value_id_lookups("p", ARGV[5]),
|
194
|
-
["parameters_id"] = set_property_value_id_lookups("a", ARGV[6]),
|
195
|
-
["host_id"] = set_property_value_id_lookups("h", ARGV[7]),
|
196
|
-
["method_id"] = set_property_value_id_lookups("m", ARGV[8])
|
197
|
-
}
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
local current_timebucket = get_timebucket(request.ts_in_seconds)
|
202
|
-
|
203
|
-
local blocked_rule = false
|
204
|
-
local blocked_category = nil
|
205
|
-
local treatment = "p"
|
206
|
-
|
207
|
-
local stream_id
|
208
|
-
|
209
|
-
if USE_TIMESTAMPS_AS_REQUEST_IDS == true then
|
210
|
-
stream_id = request.ts_in_milliseconds
|
211
|
-
else
|
212
|
-
stream_id = "*"
|
213
|
-
end
|
214
|
-
|
215
|
-
local stream_args = {
|
216
|
-
"XADD",
|
217
|
-
"rStream",
|
218
|
-
"MINID",
|
219
|
-
tostring((current_timebucket - EXPIRATION_IN_SECONDS) * 1000 ),
|
220
|
-
stream_id,
|
221
|
-
"i", request.ip_id,
|
222
|
-
"u", request.user_agent_id,
|
223
|
-
"p", request.path_id,
|
224
|
-
"h", request.host_id,
|
225
|
-
"m", request.method_id,
|
226
|
-
"a", request.parameters_id,
|
227
|
-
}
|
228
|
-
|
229
|
-
local allowed_rule, allowed_category = check_allowed(request)
|
230
|
-
|
231
|
-
if allowed_rule then
|
232
|
-
table.insert(stream_args, "t")
|
233
|
-
table.insert(stream_args, "a")
|
234
|
-
|
235
|
-
treatment = "a"
|
236
|
-
|
237
|
-
table.insert(stream_args, "ac")
|
238
|
-
table.insert(stream_args, allowed_category)
|
239
|
-
|
240
|
-
table.insert(stream_args, "ar")
|
241
|
-
table.insert(stream_args, allowed_rule)
|
242
|
-
|
243
|
-
else
|
244
|
-
blocked_rule, blocked_category = check_blocks(request)
|
245
|
-
end
|
246
|
-
|
247
|
-
if blocked_rule then
|
248
|
-
table.insert(stream_args, "t")
|
249
|
-
table.insert(stream_args, "b")
|
250
|
-
|
251
|
-
treatment = "b"
|
252
|
-
|
253
|
-
table.insert(stream_args, "bc")
|
254
|
-
table.insert(stream_args, blocked_category)
|
255
|
-
|
256
|
-
table.insert(stream_args, "br")
|
257
|
-
table.insert(stream_args, blocked_rule)
|
258
|
-
end
|
259
|
-
|
260
|
-
if blocked_rule == false and allowed_rule == false then
|
261
|
-
table.insert(stream_args, "t")
|
262
|
-
table.insert(stream_args, "p")
|
263
|
-
end
|
264
|
-
|
265
|
-
local request_id = redis.call(unpack(stream_args))
|
266
|
-
|
267
|
-
increment_leaderboard_for("i", request.ip_id, current_timebucket)
|
268
|
-
increment_leaderboard_for("u", request.user_agent_id, current_timebucket)
|
269
|
-
increment_leaderboard_for("p", request.path_id, current_timebucket)
|
270
|
-
increment_leaderboard_for("a", request.parameters_id, current_timebucket)
|
271
|
-
increment_leaderboard_for("h", request.host_id, current_timebucket)
|
272
|
-
increment_leaderboard_for("m", request.method_id, current_timebucket)
|
273
|
-
increment_leaderboard_for("t", treatment, current_timebucket)
|
274
|
-
|
275
|
-
set_property_to_requests_list("i", request.ip_id, request_id, current_timebucket)
|
276
|
-
set_property_to_requests_list("u", request.user_agent_id, request_id, current_timebucket)
|
277
|
-
set_property_to_requests_list("p", request.path_id, request_id, current_timebucket)
|
278
|
-
set_property_to_requests_list("a", request.parameters_id, request_id, current_timebucket)
|
279
|
-
set_property_to_requests_list("h", request.host_id, request_id, current_timebucket)
|
280
|
-
set_property_to_requests_list("m", request.method_id, request_id, current_timebucket)
|
281
|
-
set_property_to_requests_list("t", treatment, request_id, current_timebucket)
|
282
|
-
|
283
|
-
if blocked_rule ~= false then
|
284
|
-
increment_leaderboard_for("bc", blocked_category, current_timebucket)
|
285
|
-
set_property_to_requests_list("bc", blocked_category, request_id, current_timebucket)
|
286
|
-
|
287
|
-
increment_leaderboard_for("br", blocked_rule, current_timebucket)
|
288
|
-
set_property_to_requests_list("br", blocked_rule, request_id, current_timebucket)
|
289
|
-
end
|
290
|
-
|
291
|
-
if allowed_rule ~= false then
|
292
|
-
increment_leaderboard_for("ac", allowed_category, current_timebucket)
|
293
|
-
set_property_to_requests_list("ac", allowed_category, request_id, current_timebucket)
|
294
|
-
|
295
|
-
increment_leaderboard_for("ar", allowed_rule, current_timebucket)
|
296
|
-
set_property_to_requests_list("ar", allowed_rule, request_id, current_timebucket)
|
297
|
-
end
|
298
|
-
|
299
|
-
if blocked_rule ~= false then
|
300
|
-
return "Blocked"
|
301
|
-
elseif allowed_rule ~= false then
|
302
|
-
return "Allowed"
|
303
|
-
else
|
304
|
-
return "Passed"
|
305
|
-
end
|