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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 031d8144c5556cf40a2325f2299edfadbe4e9f1528d8150df8d87548b47ace09
4
- data.tar.gz: baa7327a7cd765ccd5ebe098490e108f1e04d4ca66a57c6c19eead5a9ba4ff84
3
+ metadata.gz: f0dfe47e533d91f974d391af755f1bd8e148808cbfe86dd6755b839201282b47
4
+ data.tar.gz: bd7135922d96fa7789cc65dbd47635c4b1e84a4e7b27a6b4dde5a1a00cc15ac8
5
5
  SHA512:
6
- metadata.gz: 8ea901efba9cb37e0756ffdb94c2f35d18927b78874da49dbb5c52d1cd06a5f152ecfac867fcd5232ae823e3809e24e6c7f7f8f1174c233cd16bb9c56d93a20f
7
- data.tar.gz: 2f38a25d6cd4a16be22bd422e47fbb8cda1d1553794daf7da2fa5190d70f3ef4a04f5e5a382b66c36741ada88562d9cdbed697821b97aaa2fa5d1646c61ea72e
6
+ metadata.gz: 0c1e66690ca97e6f9a9349f1960f92f5376175660bebc9a172eabe849af6806d4c5f278f1765a22b36ce75797a757bf9cb3719a58446aaa5fa8fe0356c348cf0
7
+ data.tar.gz: 35a3e86f1f3c63ec38c3d67adbfb952fa89a062dad78450bfd365f45c1a1607f689e104e2c7ef6faac3a00461e07022d041e5d2f667396ad81baa3af30be8acf
@@ -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
- attr_accessor :redis
8
- attr_accessor :redis_pool_size
9
- attr_accessor :maxmemory
10
- attr_accessor :quiet_mode
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
- def connection_pool
19
- @connection_pool ||=
20
- ConnectionPool.new(size: redis_pool_size) { redis }
21
- end
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
- def create_settings
24
- redis.hset('waf-settings',
25
- 'version', Wafris::VERSION,
26
- 'client', 'ruby',
27
- 'maxmemory', @maxmemory)
28
- LogSuppressor.puts_log(
29
- "[Wafris] firewall enabled. Connected to Redis on #{redis.connection[:host]}. Ready to process requests. Set rules at: https://wafris.org/hub"
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
- def core_sha
34
- @core_sha ||= redis.script(:load, wafris_core)
35
- end
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
- def wafris_core
38
- read_lua_dist("wafris_core")
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
- private
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 file_path(filename)
50
- File.join(
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
@@ -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
- if Wafris.allow_request?(request)
30
- @app.call(env)
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
- LogSuppressor.puts_log(
33
- "[Wafris] Blocked: #{request.ip} #{request.request_method} #{request.host} #{request.url}}"
34
- )
35
- [403, {}, ['Blocked']]
68
+ #ap request
69
+ [500, { 'content-type' => 'text/plain' }, ['Error']]
36
70
  end
37
- rescue Redis::TimeoutError
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
- LogSuppressor.puts_log(
44
- "[Wafris] Redis connection error: #{e.message}. Request passed without rules check."
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Wafris
4
- VERSION = "1.1.10"
4
+ VERSION = "2.0.0"
5
5
  end
data/lib/wafris.rb CHANGED
@@ -1,61 +1,605 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'connection_pool'
3
+
4
4
  require 'rails'
5
- require 'redis'
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
- raise ArgumentError unless block_given?
19
-
20
- self.configuration ||= Wafris::Configuration.new
21
- yield(configuration)
22
- LogSuppressor.puts_log(
23
- "[Wafris] attempting firewall connection via Wafris.configure initializer."
24
- ) unless configuration.quiet_mode
25
- configuration.create_settings
26
- rescue ArgumentError
27
- LogSuppressor.puts_log(
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
- if status.eql? 'Blocked'
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
- return true
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: 1.1.10
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
- - Micahel Buckbee
7
+ - Michael Buckbee
8
8
  - Ryan Castillo
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2024-03-05 00:00:00.000000000 Z
12
+ date: 2024-07-17 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
- name: connection_pool
15
+ name: rack
16
16
  requirement: !ruby/object:Gem::Requirement
17
17
  requirements:
18
18
  - - ">="
19
19
  - !ruby/object:Gem::Version
20
- version: '2.3'
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.3'
27
+ version: '2.0'
28
28
  - !ruby/object:Gem::Dependency
29
- name: rack
29
+ name: sqlite3
30
30
  requirement: !ruby/object:Gem::Requirement
31
31
  requirements:
32
32
  - - ">="
33
33
  - !ruby/object:Gem::Version
34
- version: '2.0'
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: '2.0'
41
+ version: '0'
42
42
  - !ruby/object:Gem::Dependency
43
- name: redis
43
+ name: ipaddr
44
44
  requirement: !ruby/object:Gem::Requirement
45
45
  requirements:
46
46
  - - ">="
47
47
  - !ruby/object:Gem::Version
48
- version: 4.8.0
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: 4.8.0
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: '5.0'
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: '5.0'
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 wafris gem.
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://github.com/Wafris/wafris-rb
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