aikido-zen 1.0.8-x86_64-mingw-64 → 1.1.0-x86_64-mingw-64
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/aikido/zen/agent.rb +25 -9
- data/lib/aikido/zen/api_client.rb +38 -5
- data/lib/aikido/zen/collector/event.rb +27 -0
- data/lib/aikido/zen/collector/stats.rb +15 -2
- data/lib/aikido/zen/collector.rb +14 -0
- data/lib/aikido/zen/config.rb +2 -0
- data/lib/aikido/zen/errors.rb +11 -0
- data/lib/aikido/zen/middleware/user_agent_checker.rb +41 -0
- data/lib/aikido/zen/rails_engine.rb +1 -0
- data/lib/aikido/zen/runtime_settings.rb +93 -12
- data/lib/aikido/zen/version.rb +1 -1
- data/lib/aikido/zen.rb +11 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7a0d1de6ccf7f8ea4df607a90ae0d121767a718f4326989ba8e98ff3b99926d6
|
|
4
|
+
data.tar.gz: 8652c38702a940c6a945cbd8d240b8bfdbcfef767ff2d4d563a349c4d3df2f57
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 266b69a58c5fabdf388d18dc6a5cce89bcd3e7d4ef958ab36675bf8828137ae05e69daee9fbe824f5a59ac88275a40883c9ebd03396e86be151ff81ca89b6d2a
|
|
7
|
+
data.tar.gz: 14de8dfc220c2040b32a4c519335002d383442ccec61f99e535494f6ce34f21c4626c9f1abcab4f7955f43fc5cd3b82b6cee948e6daf27f0852a26fb3e6cce8d
|
data/lib/aikido/zen/agent.rb
CHANGED
|
@@ -46,21 +46,30 @@ module Aikido::Zen
|
|
|
46
46
|
if Aikido::Zen.blocking_mode?
|
|
47
47
|
@config.logger.info("Requests identified as attacks will be blocked")
|
|
48
48
|
else
|
|
49
|
-
@config.logger.warn("Non-blocking mode enabled! No requests will be blocked
|
|
49
|
+
@config.logger.warn("Non-blocking mode enabled! No requests will be blocked")
|
|
50
50
|
end
|
|
51
51
|
|
|
52
52
|
if @api_client.can_make_requests?
|
|
53
|
-
@config.logger.info("API Token set! Reporting has been enabled
|
|
53
|
+
@config.logger.info("API Token set! Reporting has been enabled")
|
|
54
54
|
else
|
|
55
|
-
@config.logger.warn("No API Token set! Reporting has been disabled
|
|
55
|
+
@config.logger.warn("No API Token set! Reporting has been disabled")
|
|
56
56
|
return
|
|
57
57
|
end
|
|
58
58
|
|
|
59
59
|
at_exit { stop! if started? }
|
|
60
60
|
|
|
61
61
|
report(Events::Started.new(time: @started_at)) do |response|
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
if Aikido::Zen.runtime_settings.update_from_runtime_config_json(response)
|
|
63
|
+
updated_settings!
|
|
64
|
+
@config.logger.info("Updated runtime settings")
|
|
65
|
+
end
|
|
66
|
+
rescue => err
|
|
67
|
+
@config.logger.error(err.message)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
begin
|
|
71
|
+
Aikido::Zen.runtime_settings.update_from_runtime_firewall_lists_json(@api_client.fetch_runtime_firewall_lists)
|
|
72
|
+
@config.logger.info("Updated runtime firewall list")
|
|
64
73
|
rescue => err
|
|
65
74
|
@config.logger.error(err.message)
|
|
66
75
|
end
|
|
@@ -148,8 +157,10 @@ module Aikido::Zen
|
|
|
148
157
|
|
|
149
158
|
heartbeat = @collector.flush
|
|
150
159
|
report(heartbeat) do |response|
|
|
151
|
-
|
|
152
|
-
|
|
160
|
+
if Aikido::Zen.runtime_settings.update_from_runtime_config_json(response)
|
|
161
|
+
updated_settings!
|
|
162
|
+
@config.logger.info("Updated runtime settings after heartbeat")
|
|
163
|
+
end
|
|
153
164
|
end
|
|
154
165
|
end
|
|
155
166
|
|
|
@@ -163,8 +174,13 @@ module Aikido::Zen
|
|
|
163
174
|
def poll_for_setting_updates
|
|
164
175
|
@worker.every(@config.polling_interval) do
|
|
165
176
|
if @api_client.should_fetch_settings?
|
|
166
|
-
|
|
167
|
-
|
|
177
|
+
if Aikido::Zen.runtime_settings.update_from_runtime_config_json(@api_client.fetch_runtime_config)
|
|
178
|
+
updated_settings!
|
|
179
|
+
@config.logger.info("Updated runtime settings after polling")
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
Aikido::Zen.runtime_settings.update_from_runtime_firewall_lists_json(@api_client.fetch_runtime_firewall_lists)
|
|
183
|
+
@config.logger.info("Updated runtime firewall list after polling")
|
|
168
184
|
end
|
|
169
185
|
end
|
|
170
186
|
end
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "net/http"
|
|
4
|
+
require "zlib"
|
|
5
|
+
require "stringio"
|
|
4
6
|
require_relative "rate_limiter"
|
|
5
7
|
|
|
6
8
|
module Aikido::Zen
|
|
@@ -43,19 +45,29 @@ module Aikido::Zen
|
|
|
43
45
|
new_updated_at > last_updated_at
|
|
44
46
|
end
|
|
45
47
|
|
|
46
|
-
# Fetches the runtime
|
|
48
|
+
# Fetches the runtime config from the server. In case of a timeout or
|
|
47
49
|
# other low-lever error, the request will be automatically retried up to two
|
|
48
50
|
# times, after which it will raise an error.
|
|
49
51
|
#
|
|
50
52
|
# @return [Hash] decoded JSON response from the server with the runtime
|
|
51
53
|
# settings.
|
|
52
54
|
# @raise (see #request)
|
|
53
|
-
def
|
|
54
|
-
@config.logger.debug("Fetching new runtime
|
|
55
|
+
def fetch_runtime_config
|
|
56
|
+
@config.logger.debug("Fetching new runtime config")
|
|
55
57
|
|
|
56
58
|
request(Net::HTTP::Get.new("/api/runtime/config", default_headers))
|
|
57
59
|
end
|
|
58
60
|
|
|
61
|
+
def fetch_runtime_firewall_lists
|
|
62
|
+
@config.logger.debug("Fetching new runtime firewall lists")
|
|
63
|
+
|
|
64
|
+
headers = default_headers.merge({
|
|
65
|
+
"Accept-Encoding" => "gzip"
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
request(Net::HTTP::Get.new("/api/runtime/firewall/lists", headers))
|
|
69
|
+
end
|
|
70
|
+
|
|
59
71
|
# @overload report(event)
|
|
60
72
|
# Reports an event to the server.
|
|
61
73
|
#
|
|
@@ -69,7 +81,7 @@ module Aikido::Zen
|
|
|
69
81
|
#
|
|
70
82
|
# @param settings_updating_event [Aikido::Zen::Events::Started,
|
|
71
83
|
# Aikido::Zen::Events::Heartbeat]
|
|
72
|
-
# @return (see #
|
|
84
|
+
# @return (see #fetch_runtime_config)
|
|
73
85
|
# @raise (see #request)
|
|
74
86
|
def report(event)
|
|
75
87
|
event_type = if event.respond_to?(:type)
|
|
@@ -116,7 +128,12 @@ module Aikido::Zen
|
|
|
116
128
|
|
|
117
129
|
case response
|
|
118
130
|
when Net::HTTPSuccess
|
|
119
|
-
|
|
131
|
+
begin
|
|
132
|
+
body = decode(response.body, response["Content-Encoding"])
|
|
133
|
+
@config.json_decoder.call(body)
|
|
134
|
+
rescue => err
|
|
135
|
+
raise DecodeError.new(request, err)
|
|
136
|
+
end
|
|
120
137
|
when Net::HTTPTooManyRequests
|
|
121
138
|
raise RateLimitedError.new(request, response)
|
|
122
139
|
else
|
|
@@ -127,6 +144,22 @@ module Aikido::Zen
|
|
|
127
144
|
raise NetworkError.new(request, err)
|
|
128
145
|
end
|
|
129
146
|
|
|
147
|
+
# @param data [String, nil]
|
|
148
|
+
# @param encoding [String, nil]
|
|
149
|
+
# @return [String, nil]
|
|
150
|
+
private def decode(data, encoding)
|
|
151
|
+
return data unless data
|
|
152
|
+
|
|
153
|
+
case encoding&.downcase
|
|
154
|
+
when "gzip"
|
|
155
|
+
StringIO.open(data, "r") do |io|
|
|
156
|
+
Zlib::GzipReader.wrap(io) { |gz| gz.read }
|
|
157
|
+
end
|
|
158
|
+
else
|
|
159
|
+
data
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
130
163
|
private def http_settings(base_url)
|
|
131
164
|
@http_settings ||= {
|
|
132
165
|
use_ssl: base_url.scheme == "https",
|
|
@@ -52,6 +52,33 @@ module Aikido::Zen
|
|
|
52
52
|
end
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
+
class TrackUserAgent < Event
|
|
56
|
+
register "track_user_agent"
|
|
57
|
+
|
|
58
|
+
def self.from_json(data)
|
|
59
|
+
new(data[:user_agent_keys])
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def initialize(user_agent_keys)
|
|
63
|
+
super()
|
|
64
|
+
@user_agent_keys = user_agent_keys
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def as_json
|
|
68
|
+
super.update({
|
|
69
|
+
user_agent_keys: @user_agent_keys
|
|
70
|
+
})
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def handle(collector)
|
|
74
|
+
collector.handle_track_user_agent(@user_agent_keys)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def inspect
|
|
78
|
+
"#<#{self.class.name} #{@user_agent_keys.inspect}>"
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
55
82
|
class TrackAttackWave < Event
|
|
56
83
|
register "track_attack_wave"
|
|
57
84
|
|
|
@@ -8,7 +8,7 @@ module Aikido::Zen
|
|
|
8
8
|
# Tracks information about how the Aikido Agent is used in the app.
|
|
9
9
|
class Collector::Stats
|
|
10
10
|
# @!visibility private
|
|
11
|
-
attr_reader :started_at, :ended_at, :requests, :aborted_requests, :attack_waves, :blocked_attack_waves, :sinks
|
|
11
|
+
attr_reader :started_at, :ended_at, :requests, :aborted_requests, :user_agents, :attack_waves, :blocked_attack_waves, :sinks
|
|
12
12
|
|
|
13
13
|
# @!visibility private
|
|
14
14
|
attr_writer :ended_at
|
|
@@ -20,6 +20,7 @@ module Aikido::Zen
|
|
|
20
20
|
@started_at = @ended_at = nil
|
|
21
21
|
@requests = 0
|
|
22
22
|
@aborted_requests = 0
|
|
23
|
+
@user_agents = Hash.new { |h, k| h[k] = 0 }
|
|
23
24
|
@attack_waves = 0
|
|
24
25
|
@blocked_attack_waves = 0
|
|
25
26
|
@sinks = Hash.new { |h, k| h[k] = Collector::SinkStats.new(k, @config) }
|
|
@@ -27,7 +28,10 @@ module Aikido::Zen
|
|
|
27
28
|
|
|
28
29
|
# @return [Boolean]
|
|
29
30
|
def empty?
|
|
30
|
-
@requests.zero? &&
|
|
31
|
+
@requests.zero? &&
|
|
32
|
+
@user_agents.empty? &&
|
|
33
|
+
@attack_waves.zero? &&
|
|
34
|
+
@sinks.empty?
|
|
31
35
|
end
|
|
32
36
|
|
|
33
37
|
# @return [Boolean]
|
|
@@ -63,6 +67,12 @@ module Aikido::Zen
|
|
|
63
67
|
@requests += 1
|
|
64
68
|
end
|
|
65
69
|
|
|
70
|
+
# @param user_agent_keys [Array<String>] the user agent keys
|
|
71
|
+
# @return [void]
|
|
72
|
+
def add_user_agent(user_agent_keys)
|
|
73
|
+
user_agent_keys&.each { |user_agent_key| @user_agents[user_agent_key] += 1 }
|
|
74
|
+
end
|
|
75
|
+
|
|
66
76
|
# @param being_blocked [Boolean] whether the Agent blocked the request
|
|
67
77
|
# @return [void]
|
|
68
78
|
def add_attack_wave(being_blocked:)
|
|
@@ -107,6 +117,9 @@ module Aikido::Zen
|
|
|
107
117
|
total: @attack_waves,
|
|
108
118
|
blocked: @blocked_attack_waves
|
|
109
119
|
}
|
|
120
|
+
},
|
|
121
|
+
userAgents: {
|
|
122
|
+
breakdown: @user_agents
|
|
110
123
|
}
|
|
111
124
|
}
|
|
112
125
|
end
|
data/lib/aikido/zen/collector.rb
CHANGED
|
@@ -88,6 +88,20 @@ module Aikido::Zen
|
|
|
88
88
|
synchronize(@stats) { |stats| stats.add_request }
|
|
89
89
|
end
|
|
90
90
|
|
|
91
|
+
# Track stats about monitored and blocked user agents
|
|
92
|
+
#
|
|
93
|
+
# @param [Array<String>, nil] the user agent keys
|
|
94
|
+
# @return [void]
|
|
95
|
+
def track_user_agent(user_agent_keys)
|
|
96
|
+
return if user_agent_keys.nil?
|
|
97
|
+
|
|
98
|
+
add_event(Events::TrackUserAgent.new(user_agent_keys))
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def handle_track_user_agent(user_agent_keys)
|
|
102
|
+
synchronize(@stats) { |stats| stats.add_user_agent(user_agent_keys) }
|
|
103
|
+
end
|
|
104
|
+
|
|
91
105
|
# Track stats about an attack detected by our scanners.
|
|
92
106
|
#
|
|
93
107
|
# @param attack [Aikido::Zen::Events::AttackWave]
|
data/lib/aikido/zen/config.rb
CHANGED
|
@@ -300,6 +300,8 @@ module Aikido::Zen
|
|
|
300
300
|
message = case blocking_type
|
|
301
301
|
when :ip
|
|
302
302
|
format("Your IP address is not allowed to access this resource. (Your IP: %s)", request.ip)
|
|
303
|
+
when :user_agent
|
|
304
|
+
"You are not allowed to access this resource because you have been identified as a bot."
|
|
303
305
|
else
|
|
304
306
|
"You are blocked by Zen."
|
|
305
307
|
end
|
data/lib/aikido/zen/errors.rb
CHANGED
|
@@ -54,6 +54,17 @@ module Aikido
|
|
|
54
54
|
# Raised whenever a response to the API results in a 429 response.
|
|
55
55
|
class RateLimitedError < APIError; end
|
|
56
56
|
|
|
57
|
+
# Raised whenever a request body cannot be decoded.
|
|
58
|
+
class DecodeError < StandardError
|
|
59
|
+
include Error
|
|
60
|
+
|
|
61
|
+
def initialize(request, cause = nil)
|
|
62
|
+
@request = request.dup
|
|
63
|
+
|
|
64
|
+
super("Error in #{request.method} #{request.path}: #{cause.message}")
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
57
68
|
class UnderAttackError < StandardError
|
|
58
69
|
include Error
|
|
59
70
|
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aikido::Zen
|
|
4
|
+
module Middleware
|
|
5
|
+
class UserAgentChecker
|
|
6
|
+
def initialize(app, zen: Aikido::Zen, config: zen.config, settings: zen.runtime_settings)
|
|
7
|
+
@app = app
|
|
8
|
+
@zen = zen
|
|
9
|
+
@config = config
|
|
10
|
+
@settings = settings
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def call(env)
|
|
14
|
+
request = Aikido::Zen::Middleware.request_from(env)
|
|
15
|
+
|
|
16
|
+
return @app.call(env) if bypassed?(request)
|
|
17
|
+
|
|
18
|
+
user_agent = request.user_agent
|
|
19
|
+
|
|
20
|
+
if @settings.blocked_user_agent?(user_agent)
|
|
21
|
+
user_agent_keys = @settings.user_agent_keys(user_agent)
|
|
22
|
+
@zen.track_user_agent(user_agent_keys)
|
|
23
|
+
|
|
24
|
+
return @config.blocked_responder.call(request, :user_agent)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
if @settings.monitored_user_agent?(user_agent)
|
|
28
|
+
user_agent_keys = @settings.user_agent_keys(user_agent)
|
|
29
|
+
@zen.track_user_agent(user_agent_keys)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
@app.call(env)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def bypassed?(request)
|
|
36
|
+
# Bypass bot blocking and monitoring for allowed IPs
|
|
37
|
+
@settings.allowed_ips.include?(request.ip)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -17,6 +17,7 @@ module Aikido::Zen
|
|
|
17
17
|
Aikido::Zen::Middleware::ForkDetector,
|
|
18
18
|
Aikido::Zen::Middleware::ContextSetter,
|
|
19
19
|
Aikido::Zen::Middleware::AllowedAddressChecker,
|
|
20
|
+
Aikido::Zen::Middleware::UserAgentChecker,
|
|
20
21
|
Aikido::Zen::Middleware::AttackProtector,
|
|
21
22
|
Aikido::Zen::Middleware::AttackWaveProtector,
|
|
22
23
|
# Request Tracker stats do not consider failed requests, so the middleware
|
|
@@ -6,12 +6,12 @@ module Aikido::Zen
|
|
|
6
6
|
#
|
|
7
7
|
# Because the RuntimeSettings object can be modified in runtime, it implements
|
|
8
8
|
# the {Observable} API, allowing you to subscribe to updates. These are
|
|
9
|
-
# triggered whenever #
|
|
10
|
-
# don't change, no update is triggered).
|
|
9
|
+
# triggered whenever #update_from_runtime_settings_json makes a change
|
|
10
|
+
# (i.e. if the settings don't change, no update is triggered).
|
|
11
11
|
#
|
|
12
12
|
# You can subscribe to changes with +#add_observer(object, func_name)+, which
|
|
13
|
-
# will call the function passing the settings as an argument
|
|
14
|
-
RuntimeSettings = Struct.new(:updated_at, :heartbeat_interval, :endpoints, :blocked_user_ids, :allowed_ips, :received_any_stats, :blocking_mode) do
|
|
13
|
+
# will call the function passing the settings as an argument
|
|
14
|
+
RuntimeSettings = Struct.new(:updated_at, :heartbeat_interval, :endpoints, :blocked_user_ids, :allowed_ips, :received_any_stats, :blocking_mode, :blocked_user_agent_regexp, :monitored_user_agent_regexp, :user_agent_details) do
|
|
15
15
|
def initialize(*)
|
|
16
16
|
super
|
|
17
17
|
self.endpoints ||= RuntimeSettings::Endpoints.new
|
|
@@ -25,10 +25,6 @@ module Aikido::Zen
|
|
|
25
25
|
# @return [Integer] duration in seconds between heartbeat requests to the
|
|
26
26
|
# Aikido server.
|
|
27
27
|
|
|
28
|
-
# @!attribute [rw] received_any_stats
|
|
29
|
-
# @return [Boolean] whether the Aikido server has received any data from
|
|
30
|
-
# this application.
|
|
31
|
-
|
|
32
28
|
# @!attribute [rw] endpoints
|
|
33
29
|
# @return [Aikido::Zen::RuntimeSettings::Endpoints]
|
|
34
30
|
|
|
@@ -38,15 +34,31 @@ module Aikido::Zen
|
|
|
38
34
|
# @!attribute [rw] allowed_ips
|
|
39
35
|
# @return [Aikido::Zen::RuntimeSettings::IPSet]
|
|
40
36
|
|
|
37
|
+
# @!attribute [rw] received_any_stats
|
|
38
|
+
# @return [Boolean] whether the Aikido server has received any data from
|
|
39
|
+
# this application.
|
|
40
|
+
|
|
41
|
+
# @!attribute [rw] blocking_mode
|
|
42
|
+
# @return [Boolean]
|
|
43
|
+
|
|
44
|
+
# @!attribute [rw] blocked_user_agent_regexp
|
|
45
|
+
# @return [Regexp]
|
|
46
|
+
|
|
47
|
+
# @!attribute [rw] monitored_user_agent_regexp
|
|
48
|
+
# @return [Regexp]
|
|
49
|
+
|
|
50
|
+
# @!attribute [rw] user_agent_details
|
|
51
|
+
# @return [Regexp]
|
|
52
|
+
|
|
41
53
|
# Parse and interpret the JSON response from the core API with updated
|
|
42
|
-
# settings, and apply the changes.
|
|
43
|
-
#
|
|
54
|
+
# runtime settings, and apply the changes.
|
|
55
|
+
#
|
|
56
|
+
# This will also notify any subscriber to updates.
|
|
44
57
|
#
|
|
45
58
|
# @param data [Hash] the decoded JSON payload from the /api/runtime/config
|
|
46
59
|
# API endpoint.
|
|
47
|
-
#
|
|
48
60
|
# @return [bool]
|
|
49
|
-
def
|
|
61
|
+
def update_from_runtime_config_json(data)
|
|
50
62
|
last_updated_at = updated_at
|
|
51
63
|
|
|
52
64
|
self.updated_at = Time.at(data["configUpdatedAt"].to_i / 1000)
|
|
@@ -59,6 +71,75 @@ module Aikido::Zen
|
|
|
59
71
|
|
|
60
72
|
updated_at != last_updated_at
|
|
61
73
|
end
|
|
74
|
+
|
|
75
|
+
# Parse and interpret the JSON response from the core API with updated
|
|
76
|
+
# runtime firewall lists, and apply the changes.
|
|
77
|
+
#
|
|
78
|
+
# @param data [Hash] the decoded JSON payload from the /api/runtime/firewall/lists
|
|
79
|
+
# API endpoint.
|
|
80
|
+
# @return [void]
|
|
81
|
+
def update_from_runtime_firewall_lists_json(data)
|
|
82
|
+
self.blocked_user_agent_regexp = pattern(data["blockedUserAgents"])
|
|
83
|
+
|
|
84
|
+
self.monitored_user_agent_regexp = pattern(data["monitoredUserAgents"])
|
|
85
|
+
|
|
86
|
+
self.user_agent_details = []
|
|
87
|
+
|
|
88
|
+
data["userAgentDetails"]&.each do |record|
|
|
89
|
+
key = record["key"]
|
|
90
|
+
pattern = pattern(record["pattern"])
|
|
91
|
+
|
|
92
|
+
next if key.nil? || pattern.nil?
|
|
93
|
+
|
|
94
|
+
user_agent_details << {
|
|
95
|
+
key: key,
|
|
96
|
+
pattern: pattern
|
|
97
|
+
}
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Construct a regular expression from the non-nil and non-empty string,
|
|
102
|
+
# otherwise return nil.
|
|
103
|
+
#
|
|
104
|
+
# The resulting regular expression is case insensitive.
|
|
105
|
+
#
|
|
106
|
+
# @param string [String, nil]
|
|
107
|
+
# @return [Regexp, nil]
|
|
108
|
+
private def pattern(string)
|
|
109
|
+
return nil if string.nil? || string.empty?
|
|
110
|
+
|
|
111
|
+
begin
|
|
112
|
+
/#{string}/i
|
|
113
|
+
rescue RegexpError
|
|
114
|
+
nil
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# @param user_agent [String] the user agent
|
|
119
|
+
# @return [Boolean] whether the user agent should be blocked
|
|
120
|
+
def blocked_user_agent?(user_agent)
|
|
121
|
+
return false if blocked_user_agent_regexp.nil?
|
|
122
|
+
|
|
123
|
+
blocked_user_agent_regexp.match?(user_agent)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# @param user_agent [String] the user agent
|
|
127
|
+
# @return [Boolean] whether the user agent should be monitored
|
|
128
|
+
def monitored_user_agent?(user_agent)
|
|
129
|
+
return false if monitored_user_agent_regexp.nil?
|
|
130
|
+
|
|
131
|
+
monitored_user_agent_regexp.match?(user_agent)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# @param user_agent [String] the user agent
|
|
135
|
+
# @return [Array<String>] the matching user agent keys
|
|
136
|
+
def user_agent_keys(user_agent)
|
|
137
|
+
return [] if user_agent_details.nil?
|
|
138
|
+
|
|
139
|
+
user_agent_details
|
|
140
|
+
.filter { |record| record[:pattern].match?(user_agent) }
|
|
141
|
+
.map { |record| record[:key] }
|
|
142
|
+
end
|
|
62
143
|
end
|
|
63
144
|
end
|
|
64
145
|
|
data/lib/aikido/zen/version.rb
CHANGED
data/lib/aikido/zen.rb
CHANGED
|
@@ -16,6 +16,7 @@ require_relative "zen/middleware/middleware"
|
|
|
16
16
|
require_relative "zen/middleware/fork_detector"
|
|
17
17
|
require_relative "zen/middleware/context_setter"
|
|
18
18
|
require_relative "zen/middleware/allowed_address_checker"
|
|
19
|
+
require_relative "zen/middleware/user_agent_checker"
|
|
19
20
|
require_relative "zen/middleware/attack_protector"
|
|
20
21
|
require_relative "zen/middleware/attack_wave_protector"
|
|
21
22
|
require_relative "zen/middleware/request_tracker"
|
|
@@ -37,7 +38,7 @@ module Aikido
|
|
|
37
38
|
# @return [void]
|
|
38
39
|
def self.protect!
|
|
39
40
|
if config.disabled?
|
|
40
|
-
config.logger.warn("Zen has been disabled and will not run
|
|
41
|
+
config.logger.warn("Zen has been disabled and will not run")
|
|
41
42
|
return
|
|
42
43
|
end
|
|
43
44
|
|
|
@@ -128,6 +129,15 @@ module Aikido
|
|
|
128
129
|
collector.track_request
|
|
129
130
|
end
|
|
130
131
|
|
|
132
|
+
# Track monitored and blocked user agents.
|
|
133
|
+
#
|
|
134
|
+
# @param user_agent_keys [Array<String>, nil] the user agent keys
|
|
135
|
+
# from matching runtime firewall list user agent details.
|
|
136
|
+
# @return [void]
|
|
137
|
+
def self.track_user_agent(user_agent_keys)
|
|
138
|
+
collector.track_user_agent(user_agent_keys)
|
|
139
|
+
end
|
|
140
|
+
|
|
131
141
|
# Track statistics about an attack wave the app is handling.
|
|
132
142
|
#
|
|
133
143
|
# @param attack_wave [Aikido::Zen::Events::AttackWave]
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: aikido-zen
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: x86_64-mingw-64
|
|
6
6
|
authors:
|
|
7
7
|
- Aikido Security
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-01-
|
|
11
|
+
date: 2026-01-13 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: concurrent-ruby
|
|
@@ -118,6 +118,7 @@ files:
|
|
|
118
118
|
- lib/aikido/zen/middleware/middleware.rb
|
|
119
119
|
- lib/aikido/zen/middleware/rack_throttler.rb
|
|
120
120
|
- lib/aikido/zen/middleware/request_tracker.rb
|
|
121
|
+
- lib/aikido/zen/middleware/user_agent_checker.rb
|
|
121
122
|
- lib/aikido/zen/outbound_connection.rb
|
|
122
123
|
- lib/aikido/zen/outbound_connection_monitor.rb
|
|
123
124
|
- lib/aikido/zen/package.rb
|