wafris 1.1.10 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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