wafris 0.0.1 → 0.2.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: dd571cbf8934f0b14384e1b85d42d5a70ffaaa98f17e8fcfd3545553b6b1d6af
4
- data.tar.gz: a5a053621c83c0ed2892a8aca08fc26439e6e0cff95bcdbf8787b35c0ad1eaf3
3
+ metadata.gz: b9ad06b7e5b23c313860cbdd76ee405cda36958112889cc591b3733270e31312
4
+ data.tar.gz: 98239b5926e3d57f732c37638ec4166ffb170d1f1f79cc1c36779f803f0c07e4
5
5
  SHA512:
6
- metadata.gz: 339e8b1fee46d79287716e20edfc24b80c720a82f12d07485508c0beb16c9813f4f35530b7473d0711d75a6e316e97236d7912024b984e96524649021d58be19
7
- data.tar.gz: c674ca5de599c5f0bb7f7bfb6beba19d4bec38411f66cddf27897c4ecafe9a3486e723b30e39a7cb63472106ca62483ee7ffc5b98fd31dd331d7eae82efc13df
6
+ metadata.gz: 1db5393514faa4b605923e039d7270f9eaddcc4d62e5b6f8897372a7b7c4c3a297e8d0900bf18acb53ebf2bb32b43c8ddf6c2ae0a2b1eca67df4ba547446d4b6
7
+ data.tar.gz: fcb42f4ba9a8d1a07f55ebdda5e76dd3e9651675b96f3d7a5ae7024978ac4e81bf35ae9d8fc1c71a67fc10f2f79e0baa8b781bf3a36eeac202fb44b7c386f040
@@ -0,0 +1,96 @@
1
+ local function get_time_bucket_from_timestamp(unix_time_milliseconds)
2
+ local function calculate_years_number_of_days(yr)
3
+ return (yr % 4 == 0 and (yr % 100 ~= 0 or yr % 400 == 0)) and 366 or 365
4
+ end
5
+
6
+ local function get_year_and_day_number(year, days)
7
+ while days >= calculate_years_number_of_days(year) do
8
+ days = days - calculate_years_number_of_days(year)
9
+ year = year + 1
10
+ end
11
+ return year, days
12
+ end
13
+
14
+ local function get_month_and_month_day(days, year)
15
+ local days_in_each_month = {
16
+ 31,
17
+ (calculate_years_number_of_days(year) == 366 and 29 or 28),
18
+ 31,
19
+ 30,
20
+ 31,
21
+ 30,
22
+ 31,
23
+ 31,
24
+ 30,
25
+ 31,
26
+ 30,
27
+ 31,
28
+ }
29
+
30
+ for month = 1, #days_in_each_month do
31
+ if days - days_in_each_month[month] <= 0 then
32
+ return month, days
33
+ end
34
+ days = days - days_in_each_month[month]
35
+ end
36
+ end
37
+
38
+ local unix_time = unix_time_milliseconds / 1000
39
+ local year = 1970
40
+ local days = math.ceil(unix_time / 86400)
41
+ local month = nil
42
+
43
+ year, days = get_year_and_day_number(year, days)
44
+ month, days = get_month_and_month_day(days, year)
45
+ local hours = math.floor(unix_time / 3600 % 24)
46
+ -- local minutes, seconds = math.floor(unix_time / 60 % 60), math.floor(unix_time % 60)
47
+ -- hours = hours > 12 and hours - 12 or hours == 0 and 12 or hours
48
+ return string.format("%04d-%02d-%02d-%02d", year, month, days, hours)
49
+ end
50
+
51
+ -- For: Relationship of IP to time of Request (Stream)
52
+ local function get_request_id(timestamp, ip, max_requests)
53
+ timestamp = timestamp or "*"
54
+ local request_id = redis.call("XADD", "ip-requests-stream", "MAXLEN", "~", max_requests, timestamp, "ip", ip)
55
+ return request_id
56
+ end
57
+
58
+ local function add_to_HLL_request_count(timebucket, request_id)
59
+ redis.call("PFADD", "unique-requests:" .. timebucket, request_id)
60
+ end
61
+
62
+ -- For: Leaderboard of IPs with Request count as score
63
+ local function increment_timebucket_for_ip(timebucket, ip)
64
+ redis.call("ZINCRBY", "ip-leader-sset:" .. timebucket, 1, ip)
65
+ end
66
+
67
+ -- Configuration
68
+ local max_requests = 100000
69
+ local max_requests_per_ip = 10000
70
+
71
+ local ip = ARGV[1]
72
+ local ip_to_decimal = ARGV[2]
73
+ local unix_time_milliseconds = ARGV[3]
74
+ local unix_time = ARGV[3] / 1000
75
+
76
+ -- Initialize local variables
77
+ local request_id = get_request_id(nil, ip, max_requests)
78
+ local current_timebucket = get_time_bucket_from_timestamp(unix_time_milliseconds)
79
+
80
+ -- GRAPH DATA COLLECTION
81
+ add_to_HLL_request_count(current_timebucket, request_id)
82
+
83
+ -- LEADERBOARD DATA COLLECTION
84
+ increment_timebucket_for_ip(current_timebucket, ip)
85
+
86
+ -- BLOCKING LOGIC
87
+ -- Safelist Range Check
88
+ if next(redis.call("ZRANGEBYSCORE", "allowed_ranges", ip_to_decimal, "+inf", "LIMIT", 0, 1)) then
89
+ return "Allowed"
90
+ -- Blocklist Range Check
91
+ elseif next(redis.call("ZRANGEBYSCORE", "blocked_ranges", ip_to_decimal, "+inf", "LIMIT", 0, 1)) then
92
+ return "Blocked"
93
+ -- No Matches
94
+ else
95
+ return "Not found"
96
+ end
@@ -0,0 +1,58 @@
1
+ function get_time_bucket_from_timestamp(unix_time_milliseconds)
2
+ local function calculate_years_number_of_days(yr)
3
+ return (yr % 4 == 0 and (yr % 100 ~= 0 or yr % 400 == 0)) and 366 or 365
4
+ end
5
+
6
+ local function get_year_and_day_number(year, days)
7
+ while days >= calculate_years_number_of_days(year) do
8
+ days = days - calculate_years_number_of_days(year)
9
+ year = year + 1
10
+ end
11
+ return year, days
12
+ end
13
+
14
+ local function get_month_and_month_day(days, year)
15
+ local days_in_each_month = {
16
+ 31,
17
+ (calculate_years_number_of_days(year) == 366 and 29 or 28),
18
+ 31,
19
+ 30,
20
+ 31,
21
+ 30,
22
+ 31,
23
+ 31,
24
+ 30,
25
+ 31,
26
+ 30,
27
+ 31,
28
+ }
29
+
30
+ for month = 1, #days_in_each_month do
31
+ if days - days_in_each_month[month] <= 0 then
32
+ return month, days
33
+ end
34
+ days = days - days_in_each_month[month]
35
+ end
36
+ end
37
+
38
+ local unix_time = unix_time_milliseconds / 1000
39
+ local year = 1970
40
+ local days = math.ceil(unix_time / 86400)
41
+ local month = nil
42
+
43
+ year, days = get_year_and_day_number(year, days)
44
+ month, days = get_month_and_month_day(days, year)
45
+ local hours = math.floor(unix_time / 3600 % 24)
46
+ -- local minutes, seconds = math.floor(unix_time / 60 % 60), math.floor(unix_time % 60)
47
+ -- hours = hours > 12 and hours - 12 or hours == 0 and 12 or hours
48
+ return string.format("%04d-%02d-%02d-%02d", year, month, days, hours)
49
+ end
50
+
51
+ function get_time_buckets(unix_time_milliseconds)
52
+ local time_buckets = {}
53
+
54
+ for i = 23, 0, -1 do
55
+ table.insert(time_buckets, get_time_bucket_from_timestamp(unix_time_milliseconds - (1000 * 60 * 60 * i)))
56
+ end
57
+ return time_buckets
58
+ end
@@ -0,0 +1,14 @@
1
+ local function num_requests(start_time, end_time)
2
+ local request_keys = redis.call('KEYS', 'unique-requests:*')
3
+ redis.call('PFMERGE', 'merged_unique-requests', unpack(request_keys))
4
+ return redis.call('PFCOUNT', 'merged_unique-requests')
5
+ end
6
+
7
+ local function unique_ips(start_time, end_time)
8
+ local ip_keys = redis.call('KEYS', 'unique-ips:*')
9
+ redis.call('PFMERGE', 'merged_unique-ips', unpack(ip_keys))
10
+ return redis.call('PFCOUNT', 'merged_unique-ips')
11
+ end
12
+
13
+ redis.debug("Request count: ", num_requests(0, 10000000))
14
+ redis.debug("IP request count: ", unique_ips(0, 10000000))
@@ -0,0 +1,104 @@
1
+ -- Template strings below are replaced with generated
2
+ -- data from the ip_data_generator.rb script
3
+ -- local ipArray = { }
4
+ -- local timestampArray = { }
5
+ -- redis.debug("Timestamp count: ", #timestampArray)
6
+
7
+ local function get_time_bucket_from_timestamp(unix_time_milliseconds)
8
+ local function calculate_years_number_of_days(yr)
9
+ return (yr % 4 == 0 and (yr % 100 ~= 0 or yr % 400 == 0)) and 366 or 365
10
+ end
11
+
12
+ local function get_year_and_day_number(year, days)
13
+ while days >= calculate_years_number_of_days(year) do
14
+ days = days - calculate_years_number_of_days(year)
15
+ year = year + 1
16
+ end
17
+ return year, days
18
+ end
19
+
20
+ local function get_month_and_month_day(days, year)
21
+ local days_in_each_month = {
22
+ 31,
23
+ (calculate_years_number_of_days(year) == 366 and 29 or 28),
24
+ 31,
25
+ 30,
26
+ 31,
27
+ 30,
28
+ 31,
29
+ 31,
30
+ 30,
31
+ 31,
32
+ 30,
33
+ 31,
34
+ }
35
+
36
+ for month = 1, #days_in_each_month do
37
+ if days - days_in_each_month[month] <= 0 then
38
+ return month, days
39
+ end
40
+ days = days - days_in_each_month[month]
41
+ end
42
+ end
43
+
44
+ local unix_time = unix_time_milliseconds / 1000
45
+ local year = 1970
46
+ local days = math.ceil(unix_time / 86400)
47
+ local month = nil
48
+
49
+ year, days = get_year_and_day_number(year, days)
50
+ month, days = get_month_and_month_day(days, year)
51
+ local hours = math.floor(unix_time / 3600 % 24)
52
+ -- local minutes, seconds = math.floor(unix_time / 60 % 60), math.floor(unix_time % 60)
53
+ -- hours = hours > 12 and hours - 12 or hours == 0 and 12 or hours
54
+ return string.format("%04d-%02d-%02d-%02d", year, month, days, hours)
55
+ end
56
+
57
+ -- For: Relationship of IP to time of Request (Stream)
58
+ local function get_request_id(timestamp, ip, max_requests)
59
+ timestamp = timestamp or "*"
60
+ local request_id = redis.call("XADD", "ip-requests-stream", "MAXLEN", "~", max_requests, timestamp, "ip", ip)
61
+ return request_id
62
+ end
63
+
64
+ local function add_to_HLL_request_count(timebucket, request_id)
65
+ redis.call("PFADD", "unique-requests:" .. timebucket, request_id)
66
+ end
67
+
68
+ -- Configuration
69
+ local max_requests = 100000
70
+ local max_requests_per_ip = 10000
71
+
72
+ -- Interior of this for loop is what should go into wafris_core.lua
73
+ for i = 1, #timestampArray do
74
+ -- Setup
75
+ local ip = ipArray[math.random(#ipArray)]
76
+ local timestamp = timestampArray[i]
77
+
78
+ local request_id = get_request_id(timestamp, ip, max_requests)
79
+
80
+ -- GRAPH DATA COLLECTION
81
+ local current_timebucket = get_time_bucket_from_timestamp(timestamp)
82
+ add_to_HLL_request_count(current_timebucket, request_id)
83
+
84
+ -- For: Looking up Requests an IP has made (Stream) / time of request
85
+ local ip_stream_key = "ip-stream:" .. ip
86
+ local ip_stream_id =
87
+ redis.call("XADD", ip_stream_key, "MAXLEN", "~", max_requests_per_ip, "*", "request_id", request_id)
88
+
89
+ -- For: Precalc of Number of Requests (Key)
90
+ local requests_count_key = "requests-count:" .. current_timebucket
91
+ redis.call("INCR", requests_count_key)
92
+
93
+ -- For: Precalc of Number of Requests from an IP (Key)
94
+ local ips_count_bucket_key = "ips-count:" .. ip .. ":" .. current_timebucket
95
+ redis.call("INCR", ips_count_bucket_key)
96
+
97
+ -- For: Precalc of Number of Unique IPs making Requests (HLL)
98
+ local ips_count_hll_key = "unique-ips:" .. current_timebucket
99
+ redis.call("PFADD", ips_count_hll_key, ip)
100
+
101
+ -- For: Leaderboard of IPs with Request count as score
102
+ local ip_leaderboard_sset_key = "ip-leader-sset:" .. current_timebucket
103
+ redis.call("ZINCRBY", ip_leaderboard_sset_key, 1, ip)
104
+ end
@@ -0,0 +1,40 @@
1
+ -- Code was pulled from https://otland.net/threads/how-convert-timestamp-to-date-type.251657/
2
+ -- An alternate solution is https://gist.github.com/markuman/e96d04139cd8acc33604
3
+ local function get_time_bucket_from_timestamp(unix_time_milliseconds)
4
+ local function calculate_years_number_of_days(yr)
5
+ return (yr % 4 == 0 and (yr % 100 ~= 0 or yr % 400 == 0)) and 366 or 365
6
+ end
7
+
8
+ local function get_year_and_day_number(year, days)
9
+ while days >= calculate_years_number_of_days(year) do
10
+ days = days - calculate_years_number_of_days(year)
11
+ year = year + 1
12
+ end
13
+ return year, days
14
+ end
15
+
16
+ local function get_month_and_month_day(days, year)
17
+ local days_in_each_month = {
18
+ 31,
19
+ (calculate_years_number_of_days(year) == 366 and 29 or 28),
20
+ 31, 30, 31,30,31,31,30,31,30,31
21
+ }
22
+
23
+ for month = 1, #days_in_each_month do
24
+ if days - days_in_each_month[month] <= 0 then return month, days end
25
+ days = days - days_in_each_month[month]
26
+ end
27
+ end
28
+
29
+ local unix_time = unix_time_milliseconds / 1000
30
+ local year = 1970
31
+ local days = math.ceil(unix_time/86400)
32
+ local month = nil
33
+
34
+ year, days = get_year_and_day_number(year, days)
35
+ month, days = get_month_and_month_day(days, year)
36
+ local hours = math.floor(unix_time / 3600 % 24)
37
+ -- local minutes, seconds = math.floor(unix_time / 60 % 60), math.floor(unix_time % 60)
38
+ -- hours = hours > 12 and hours - 12 or hours == 0 and 12 or hours
39
+ return string.format("%04d-%02d-%02d-%02d", year, month, days, hours)
40
+ end
@@ -28,15 +28,21 @@ module Wafris
28
28
  CONNECTION_ERROR
29
29
  end
30
30
 
31
- def script_sha
32
- @script_sha ||= redis.script(:load, wafris_core)
31
+ def core_sha
32
+ @core_sha ||= redis.script(:load, wafris_core)
33
33
  end
34
34
 
35
35
  def wafris_core
36
+ read_lua_dist("wafris_core")
37
+ end
38
+
39
+ private
40
+
41
+ def read_lua_dist(filename)
36
42
  File.read(
37
43
  File.join(
38
44
  File.dirname(__FILE__),
39
- 'wafris_core.lua'
45
+ "../lua/dist/#{filename}.lua"
40
46
  )
41
47
  )
42
48
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Wafris
4
- VERSION = "0.0.1"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/wafris.rb CHANGED
@@ -31,14 +31,13 @@ module Wafris
31
31
 
32
32
  def allow_request?(request)
33
33
  configuration.connection_pool.with do |conn|
34
- time = Time.now
34
+ time = Time.now.to_f * 1000
35
35
  status = conn.evalsha(
36
- configuration.script_sha,
36
+ configuration.core_sha,
37
37
  argv: [
38
38
  request.ip,
39
39
  IPAddr.new(request.ip).to_i,
40
- time.to_i,
41
- "all-ips:#{time.strftime('%Y-%m-%d')}:#{time.hour}"
40
+ time.to_i
42
41
  ]
43
42
  )
44
43
 
metadata CHANGED
@@ -1,15 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: wafris
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Micahel Buckbee
8
8
  - Ryan Castillo
9
- autorequire:
9
+ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2023-02-06 00:00:00.000000000 Z
12
+ date: 2023-05-17 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: connection_pool
@@ -179,23 +179,27 @@ dependencies:
179
179
  - - ">="
180
180
  - !ruby/object:Gem::Version
181
181
  version: 13.0.6
182
- description:
183
- email:
182
+ description:
183
+ email:
184
184
  executables: []
185
185
  extensions: []
186
186
  extra_rdoc_files: []
187
187
  files:
188
+ - lib/lua/dist/wafris_core.lua
189
+ - lib/lua/src/get_time_buckets.lua
190
+ - lib/lua/src/queries.lua
191
+ - lib/lua/src/seeds/data_load.lua
192
+ - lib/lua/src/time_bucket.lua
188
193
  - lib/wafris.rb
189
194
  - lib/wafris/configuration.rb
190
195
  - lib/wafris/middleware.rb
191
196
  - lib/wafris/railtie.rb
192
197
  - lib/wafris/version.rb
193
- - lib/wafris/wafris_core.lua
194
- homepage:
198
+ homepage:
195
199
  licenses:
196
- - MIT
200
+ - Elastic-2.0
197
201
  metadata: {}
198
- post_install_message:
202
+ post_install_message:
199
203
  rdoc_options: []
200
204
  require_paths:
201
205
  - lib
@@ -211,7 +215,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
211
215
  version: '0'
212
216
  requirements: []
213
217
  rubygems_version: 3.3.26
214
- signing_key:
218
+ signing_key:
215
219
  specification_version: 4
216
220
  summary: Web application firewall for Rack apps
217
221
  test_files: []
@@ -1,42 +0,0 @@
1
- local LAST_REQUESTS_TIME = 'last_requests_time'
2
- local TWENTY_FOUR_HOURS = 86400
3
-
4
- local ip = ARGV[1]
5
- local ip_to_decimal = ARGV[2]
6
- local unix_time = ARGV[3]
7
- local expire_time = unix_time - TWENTY_FOUR_HOURS
8
- local ip_request_string = "ip-requests-" .. ip
9
- local hour_bucket = ARGV[4]
10
-
11
- -- LEADERBOARD DATA COLLECTION
12
- -- Add IP to last_requests_time key by integer timestamp
13
- -- ZADD last_requets_time 1661356145 '192.168.1.1'
14
- redis.call('ZADD', LAST_REQUESTS_TIME, unix_time, ip)
15
- -- Remove IP from last_requests_time if it has been there for 24 hours
16
- -- ZREMRANGEBYSCORE last_requests_time 0 (1661356145 - 86400)
17
- redis.call('ZREMRANGEBYSCORE', LAST_REQUESTS_TIME, 0, expire_time)
18
- -- Add IP to ip-requests-<ip> for leaderboard tracking
19
- -- LPUSH ip-requests-192.168.1.1 1661356145
20
- redis.call('LPUSH', ip_request_string, unix_time)
21
- -- Have the key expire in 24 hours
22
- -- EXPIRE ip-requests-192.168.1.1 86400
23
- redis.call('EXPIRE', ip_request_string, TWENTY_FOUR_HOURS)
24
-
25
- -- GRAPH DATA COLLECTION
26
- -- Increment counter for hourly buckets
27
- -- INC all-ips:2022-10-01:12
28
- redis.call('INCR', hour_bucket)
29
- -- EXPIRE all-ips:2022-10-01:12 86400
30
- redis.call('EXPIRE', hour_bucket, TWENTY_FOUR_HOURS)
31
-
32
- -- BLOCKING LOGIC
33
- -- Safelist Range Check
34
- if next(redis.call('ZRANGEBYSCORE', 'allowed_ranges', ip_to_decimal, "+inf", "LIMIT", 0, 1)) then
35
- return 'Allowed'
36
- -- Blocklist Range Check
37
- elseif next(redis.call('ZRANGEBYSCORE', 'blocked_ranges', ip_to_decimal, "+inf", "LIMIT", 0, 1)) then
38
- return 'Blocked'
39
- -- No Matches
40
- else
41
- return 'Not found'
42
- end