aikido-zen 1.7.2 → 1.9.0.beta.2

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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +8 -1
  3. data/benchmarks/ip_list/benchmark.rb +1 -1
  4. data/docs/custom-event-tracking.md +33 -0
  5. data/docs/request-bypassing.md +16 -4
  6. data/lib/aikido/zen/agent.rb +35 -16
  7. data/lib/aikido/zen/api_client.rb +2 -0
  8. data/lib/aikido/zen/attack_wave/helpers.rb +0 -1
  9. data/lib/aikido/zen/collector/event.rb +12 -6
  10. data/lib/aikido/zen/collector/sink_stats.rb +4 -0
  11. data/lib/aikido/zen/collector/stats.rb +6 -2
  12. data/lib/aikido/zen/collector.rb +6 -6
  13. data/lib/aikido/zen/config.rb +27 -5
  14. data/lib/aikido/zen/event.rb +26 -0
  15. data/lib/aikido/zen/{runtime_settings → firewall}/ip_list.rb +1 -1
  16. data/lib/aikido/zen/firewall.rb +132 -0
  17. data/lib/aikido/zen/middleware/fork_detector.rb +10 -4
  18. data/lib/aikido/zen/middleware/ip_list_checker.rb +5 -5
  19. data/lib/aikido/zen/middleware/user_agent_checker.rb +6 -6
  20. data/lib/aikido/zen/runtime_settings.rb +1 -150
  21. data/lib/aikido/zen/scanners/path_traversal/helpers.rb +5 -1
  22. data/lib/aikido/zen/scanners/ssrf/private_ip_checker.rb +37 -2
  23. data/lib/aikido/zen/sink.rb +8 -3
  24. data/lib/aikido/zen/sinks/async_http.rb +2 -4
  25. data/lib/aikido/zen/sinks/curb.rb +2 -4
  26. data/lib/aikido/zen/sinks/em_http.rb +2 -4
  27. data/lib/aikido/zen/sinks/excon.rb +2 -4
  28. data/lib/aikido/zen/sinks/file.rb +1 -1
  29. data/lib/aikido/zen/sinks/http.rb +2 -4
  30. data/lib/aikido/zen/sinks/httpclient.rb +2 -4
  31. data/lib/aikido/zen/sinks/httpx.rb +2 -4
  32. data/lib/aikido/zen/sinks/kernel.rb +1 -1
  33. data/lib/aikido/zen/sinks/mysql2.rb +1 -1
  34. data/lib/aikido/zen/sinks/net_http.rb +2 -4
  35. data/lib/aikido/zen/sinks/patron.rb +2 -4
  36. data/lib/aikido/zen/sinks/pg.rb +1 -1
  37. data/lib/aikido/zen/sinks/resolv.rb +1 -1
  38. data/lib/aikido/zen/sinks/socket.rb +11 -9
  39. data/lib/aikido/zen/sinks/sqlite3.rb +1 -1
  40. data/lib/aikido/zen/sinks/trilogy.rb +1 -1
  41. data/lib/aikido/zen/sinks/typhoeus.rb +2 -4
  42. data/lib/aikido/zen/version.rb +1 -1
  43. data/lib/aikido/zen/worker_process/agent/client.rb +2 -2
  44. data/lib/aikido/zen.rb +83 -9
  45. metadata +5 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 41f1982e134a806c513cc166247059c3f290f6bbc88ff32dcf4bdc65da9f377d
4
- data.tar.gz: d97dac2f830555239836a143b815ca3c85c4993776183ce73bc5dc392fed9618
3
+ metadata.gz: d41298c1b3dc0135c211b4beade2a723603cef96372b5526447e5a0d792d1bb9
4
+ data.tar.gz: 247dea5c7895db42823c445ce7e0f32995ba833cecbcf7430ad09f33c897c179
5
5
  SHA512:
6
- metadata.gz: 526140c45a47c29f4f36e1508bc3305bafe535e92479c877deeb92a1f74c618fa19d7c46a7d67183310fbc414f05912b93c88f2cdb109730d04c0b4b6a54b301
7
- data.tar.gz: 6c43bc7461a52ded8331b6ba2188afd9d273e1b012f5912b2d9786c439e2890380003779925bd3f37fdd4422f4b34aefe13af6d29d0a6a7f3b4e351c5019dca1
6
+ metadata.gz: ada3f8112966d020346b164380a2cbff9e1ba7c68cfdde2ab2a440d41e1c241c6ef73c9214597fd004e408b61c00ecc9b78294bfd73bef0effd1ca91dbdee5b4
7
+ data.tar.gz: 7f61d126b05f54339ca7633e8058e36d298dcd0a4b0abf93eebe4abb96a5c66edbf0e56cc3e6457ed92f8cf46231d6f00f60dc5b8487bdd014532db48ef52dba
data/README.md CHANGED
@@ -39,6 +39,12 @@ Zen for Ruby 2.7+ is compatible with:
39
39
 
40
40
  * ✅ [Ruby on Rails](docs/rails.md) 7.x, 8.x
41
41
 
42
+ ### Application servers
43
+
44
+ * ✅ [Puma](https://puma.io/)
45
+
46
+ Our test suite only covers Puma. If you use another Rack server, [contact us](docs/troubleshooting.md#contact-support) and we'll help check whether Zen is compatible.
47
+
42
48
  ### Database drivers
43
49
 
44
50
  * ✅ [`sqlite3`](https://github.com/sparklemotion/sqlite3-ruby) 1.x, 2.x
@@ -117,7 +123,8 @@ See [Reporting to Aikido](#reporting-to-your-aikido-security-dashboard) to learn
117
123
 
118
124
  ## Additional configuration
119
125
 
120
- [Configure Zen using environment variables for authentication, mode settings, debugging, and more.](https://help.aikido.dev/doc/configuration-via-env-vars/docrSItUkeR9)
126
+ * [Configure Zen using environment variables for authentication, mode settings, debugging, and more](https://help.aikido.dev/doc/configuration-via-env-vars/docrSItUkeR9)
127
+ * [Track custom events](docs/custom-event-tracking.md): trigger events that Playbooks can act on
121
128
 
122
129
  ## License
123
130
 
@@ -24,7 +24,7 @@ end
24
24
  if __FILE__ == $0
25
25
  ip_ranges = random_ip_ranges.take(1000)
26
26
 
27
- ip_list = Aikido::Zen::RuntimeSettings::IPList.from_json({
27
+ ip_list = Aikido::Zen::Firewall::IPList.from_json({
28
28
  "key" => "key",
29
29
  "source" => "source",
30
30
  "description" => "description",
@@ -0,0 +1,33 @@
1
+ # Track custom events
2
+
3
+ Use `Aikido::Zen.track_custom_event` to report events that only your application knows about, such as failed logins. [Playbooks](https://help.aikido.dev/zen-firewall/zen-features/playbooks) can act when an event occurs repeatedly, for example by blocking an IP after three failed logins in five minutes.
4
+
5
+ ```ruby
6
+ # app/controllers/application_controller.rb
7
+ class ApplicationController < ActionController::Base
8
+ private
9
+
10
+ def authenticate_user!
11
+ # Your authentication logic here
12
+ # ...
13
+
14
+ unless current_user
15
+ Aikido::Zen.track_custom_event("user.login_failed")
16
+ return
17
+ end
18
+
19
+ Aikido::Zen.set_user(
20
+ id: current_user.id,
21
+ name: current_user.name
22
+ )
23
+
24
+ Aikido::Zen.track_custom_event("user.login_succeeded")
25
+ end
26
+ end
27
+ ```
28
+
29
+ After adding `Aikido::Zen.track_custom_event`, trigger the event at least once. It will then appear on the Playbooks page in the Aikido dashboard. From there, you can create a playbook and choose what should happen when the event occurs. Calling `Aikido::Zen.track_custom_event` by itself does not create a playbook or block anything.
30
+
31
+ Call `Aikido::Zen.track_custom_event` while handling an HTTP request. Zen associates the event with the request's IP address. Playbook counts are per IP, not across your whole app. If you call [`Aikido::Zen.set_user`](./rails.md#rate-limiting-and-user-blocking) before tracking the event, Zen also includes the current user. `Aikido::Zen.set_user` is optional. Events without a user are still tracked.
32
+
33
+ Event names can use any format. We recommend lowercase, dot-separated names such as `user.login_failed`.
@@ -1,11 +1,23 @@
1
1
  # Bypass Zen for a specific request
2
2
 
3
- To disable Zen for a specific request, call `Aikido::Zen.request_bypassed!` from a Rack middleware. This bypasses Zen's security checks and stats collection for that request.
3
+ Call `Aikido::Zen.request_bypassed!` from a Rack middleware to bypass Zen for this request. A bypassed request is fully excluded from Zen inspection and enforcement: Zen will not analyze the request, generate findings, or apply blocking rules for that traffic. Your application handles the request normally.
4
4
 
5
5
  > [!NOTE]
6
- > Zen already has a built-in feature for bypassing specific IP addresses. This feature offers more flexibility, letting you bypass Zen for a request based on any criteria you choose.
6
+ > Zen's built-in [Bypassed IPs](https://help.aikido.dev/zen-firewall/zen-features/bypassed-ips) feature uses request bypassing internally, triggered by a matching IP/CIDR. `Aikido::Zen.request_bypassed!` lets you bypass requests using your custom logic.
7
7
 
8
- `Aikido::Zen.request_bypassed!` needs the request's context to already be set up, so your middleware must run after Zen's `ContextSetter` middleware. The following example shows how to bypass a specific request in a Rails application:
8
+ ## What gets bypassed
9
+
10
+ * **Attack protection** — SQL injection, path traversal, command injection, and SSRF attacks are not blocked or reported
11
+ * **Rate limiting** — never triggered.
12
+ * **IP blocking** — Known Threat Actors, Tor traffic blocking/monitoring, country blocking, and custom IP allow/block lists are not checked.
13
+ * **Bot traffic blocking** — not checked.
14
+ * **User blocking** — blocked users are not blocked.
15
+ * **Statistics** — the request isn't counted, and doesn't count against your monitored request quota.
16
+ * **Attack wave protection** — the request doesn't count towards wave detection.
17
+
18
+ ## Usage
19
+
20
+ Insert your middleware directly after `Aikido::Zen::Middleware::ContextSetter`, so it runs after the request context is set up but before Zen's other middleware:
9
21
 
10
22
  ```ruby
11
23
  # config/application.rb
@@ -35,4 +47,4 @@ end
35
47
  ```
36
48
 
37
49
  > [!WARNING]
38
- > Use this feature with caution, as it can potentially expose your application to security risks if not used properly.
50
+ > A bypassed request gets zero protection from Zen — no attack detection, no rate limiting, no blocking, no tracking. Ensure that your custom logic only bypasses requests that you fully trust.
@@ -68,27 +68,22 @@ module Aikido::Zen
68
68
  # :nocov:
69
69
 
70
70
  report(Events::Started.new(time: @started_at)) do |response|
71
- if update_settings_from_runtime_config!(response, reason: "after start")
72
- updated_settings!
73
-
74
- if @config.realtime_settings_updates_enabled? || @settings.realtime_settings_updates_enabled?
75
- @api_stream.handle("config-updated") do |event|
76
- @config.logger.debug("Received server-sent event: config-updated")
77
- settings_updated(event)
78
- end
71
+ update_settings_from_runtime_config!(response, reason: "after start")
72
+ updated_settings!
79
73
 
80
- @api_stream.start!
74
+ if @config.realtime_settings_updates_enabled? || @settings.realtime_settings_updates_enabled?
75
+ @api_stream.handle("config-updated") do |event|
76
+ @config.logger.debug("Received server-sent event: config-updated")
77
+ settings_updated(event)
81
78
  end
82
- # :nocov:
83
- else
84
- # empty
79
+
80
+ @api_stream.start!
85
81
  end
86
- # :nocov:
87
82
  rescue => err
88
83
  @config.logger.error(err.message)
89
84
  end
90
85
 
91
- begin
86
+ @worker.perform do
92
87
  update_settings_from_runtime_firewall_lists!(@api_client.fetch_runtime_firewall_lists, reason: "after start")
93
88
  rescue => err
94
89
  @config.logger.error(err.message)
@@ -165,6 +160,15 @@ module Aikido::Zen
165
160
  end
166
161
  end
167
162
 
163
+ # Reports a custom event tracked via Aikido::Zen.track_custom_event, if
164
+ # reporting is enabled.
165
+ #
166
+ # @param event [Aikido::Zen::Events::Custom]
167
+ # @return [void]
168
+ def report_custom_event(event)
169
+ report(event) if @api_client.can_make_requests?
170
+ end
171
+
168
172
  # @api private
169
173
  #
170
174
  # Atomically flushes all the stats stored by the agent, and sends a
@@ -220,6 +224,8 @@ module Aikido::Zen
220
224
  updated_at = Time.at(event[:data]["configUpdatedAt"].to_i)
221
225
 
222
226
  if should_fetch_settings?(updated_at)
227
+ return if realtime_settings_updates_too_fast?
228
+
223
229
  if update_settings_from_runtime_config!(@api_client.fetch_runtime_config, reason: "after server-sent event")
224
230
  updated_settings!
225
231
 
@@ -239,6 +245,19 @@ module Aikido::Zen
239
245
  updated_at > last_updated_at
240
246
  end
241
247
 
248
+ def realtime_settings_updates_too_fast?
249
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
250
+
251
+ last_updated_at = @realtime_settings_updates_last_updated_at
252
+ if last_updated_at && now - last_updated_at < @config.realtime_settings_updates_min_time_between_events
253
+ return true
254
+ end
255
+
256
+ @realtime_settings_updates_last_updated_at = now
257
+
258
+ false
259
+ end
260
+
242
261
  def heartbeats
243
262
  @heartbeats ||= Aikido::Zen::Agent::HeartbeatsManager.new(
244
263
  config: @config,
@@ -257,7 +276,7 @@ module Aikido::Zen
257
276
  begin
258
277
  return false unless Aikido::Zen.api_cache.update_runtime_config(data)
259
278
 
260
- if Aikido::Zen.runtime_settings.update_from_runtime_config_json(data)
279
+ if Aikido::Zen.runtime_settings.update_from_json(data)
261
280
  @config.logger.info("Updated runtime settings #{reason}")
262
281
  true
263
282
  else
@@ -279,7 +298,7 @@ module Aikido::Zen
279
298
  begin
280
299
  return false unless Aikido::Zen.api_cache.update_runtime_firewall_lists(data)
281
300
 
282
- if Aikido::Zen.runtime_settings.update_from_runtime_firewall_lists_json(data)
301
+ if Aikido::Zen.firewall.update_from_json(data)
283
302
  @config.logger.info("Updated runtime firewall list #{reason}")
284
303
  true
285
304
  else
@@ -133,6 +133,8 @@ module Aikido::Zen
133
133
  response = http.request(request)
134
134
 
135
135
  case response
136
+ when Net::HTTPNoContent
137
+ # empty
136
138
  when Net::HTTPSuccess
137
139
  begin
138
140
  body = decode(response.body, response["Content-Encoding"])
@@ -447,7 +447,6 @@ module Aikido::Zen
447
447
  "smb.conf",
448
448
  "iis.log",
449
449
  "pom.xml",
450
- "openapi.json",
451
450
  "vim_settings.xml",
452
451
  "winscp.ini",
453
452
  "ws_ftp.ini"
@@ -157,14 +157,16 @@ module Aikido::Zen
157
157
  def self.from_json(data)
158
158
  new(
159
159
  data["sink_name"],
160
+ data["sink_kind"],
160
161
  data["duration"],
161
162
  has_errors: data["has_errors"]
162
163
  )
163
164
  end
164
165
 
165
- def initialize(sink_name, duration, has_errors:)
166
+ def initialize(sink_name, sink_kind, duration, has_errors:)
166
167
  super()
167
168
  @sink_name = sink_name
169
+ @sink_kind = sink_kind
168
170
  @duration = duration
169
171
  @has_errors = has_errors
170
172
  end
@@ -172,17 +174,18 @@ module Aikido::Zen
172
174
  def as_json
173
175
  super.update({
174
176
  "sink_name" => @sink_name,
177
+ "sink_kind" => @sink_kind,
175
178
  "duration" => @duration,
176
179
  "has_errors" => @has_errors
177
180
  })
178
181
  end
179
182
 
180
183
  def handle(collector)
181
- collector.handle_track_scan(@sink_name, @duration, has_errors: @has_errors)
184
+ collector.handle_track_scan(@sink_name, @sink_kind, @duration, has_errors: @has_errors)
182
185
  end
183
186
 
184
187
  def inspect
185
- "#<#{self.class.name} #{@sink_name} #{format "%0.6f", @duration} #{@has_errors}>"
188
+ "#<#{self.class.name} #{@sink_name} #{@sink_kind} #{format "%0.6f", @duration} #{@has_errors}>"
186
189
  end
187
190
  end
188
191
 
@@ -192,29 +195,32 @@ module Aikido::Zen
192
195
  def self.from_json(data)
193
196
  new(
194
197
  data["sink_name"],
198
+ data["sink_kind"],
195
199
  being_blocked: data["being_blocked"]
196
200
  )
197
201
  end
198
202
 
199
- def initialize(sink_name, being_blocked:)
203
+ def initialize(sink_name, sink_kind, being_blocked:)
200
204
  super()
201
205
  @sink_name = sink_name
206
+ @sink_kind = sink_kind
202
207
  @being_blocked = being_blocked
203
208
  end
204
209
 
205
210
  def as_json
206
211
  super.update({
207
212
  "sink_name" => @sink_name,
213
+ "sink_kind" => @sink_kind,
208
214
  "being_blocked" => @being_blocked
209
215
  })
210
216
  end
211
217
 
212
218
  def handle(collector)
213
- collector.handle_track_attack(@sink_name, being_blocked: @being_blocked)
219
+ collector.handle_track_attack(@sink_name, @sink_kind, being_blocked: @being_blocked)
214
220
  end
215
221
 
216
222
  def inspect
217
- "#<#{self.class.name} #{@sink_name} #{@being_blocked}>"
223
+ "#<#{self.class.name} #{@sink_name} #{@sink_kind} #{@being_blocked}>"
218
224
  end
219
225
  end
220
226
 
@@ -7,6 +7,9 @@ module Aikido::Zen
7
7
  #
8
8
  # Tracks data specific to a single Sink.
9
9
  class Collector::SinkStats
10
+ # @return [String] category of operation this sink represents.
11
+ attr_accessor :kind
12
+
10
13
  # @return [Integer] number of total calls to Sink#scan.
11
14
  attr_accessor :scans
12
15
 
@@ -62,6 +65,7 @@ module Aikido::Zen
62
65
 
63
66
  def as_json
64
67
  {
68
+ "kind" => @kind,
65
69
  "total" => @scans,
66
70
  "interceptorThrewError" => @errors,
67
71
  "withoutContext" => 0,
@@ -94,21 +94,25 @@ module Aikido::Zen
94
94
  end
95
95
 
96
96
  # @param sink_name [String] the name of the sink
97
+ # @param sink_kind [String] the category of operation the sink represents
97
98
  # @param duration [Float] the length the scan in seconds
98
99
  # @param has_errors [Boolean] whether errors occurred during the scan
99
100
  # @return [void]
100
- def add_scan(sink_name, duration, has_errors:)
101
+ def add_scan(sink_name, sink_kind, duration, has_errors:)
101
102
  stats = @sinks[sink_name]
103
+ stats.kind = sink_kind
102
104
  stats.scans += 1
103
105
  stats.errors += 1 if has_errors
104
106
  stats.add_timing(duration)
105
107
  end
106
108
 
107
109
  # @param sink_name [String] the name of the sink
110
+ # @param sink_kind [String] the category of operation the sink represents
108
111
  # @param being_blocked [Boolean] whether the Agent blocked the request
109
112
  # @return [void]
110
- def add_attack(sink_name, being_blocked:)
113
+ def add_attack(sink_name, sink_kind, being_blocked:)
111
114
  stats = @sinks[sink_name]
115
+ stats.kind = sink_kind
112
116
  stats.attacks += 1
113
117
  stats.blocked_attacks += 1 if being_blocked
114
118
  end
@@ -146,12 +146,12 @@ module Aikido::Zen
146
146
  # @param scan [Aikido::Zen::Scan]
147
147
  # @return [void]
148
148
  def track_scan(scan)
149
- add_event(Events::TrackScan.new(scan.sink.name, scan.duration, has_errors: scan.errors?))
149
+ add_event(Events::TrackScan.new(scan.sink.name, scan.sink.kind, scan.duration, has_errors: scan.errors?))
150
150
  end
151
151
 
152
- def handle_track_scan(sink_name, duration, has_errors:)
152
+ def handle_track_scan(sink_name, sink_kind, duration, has_errors:)
153
153
  synchronize(@stats) do |stats|
154
- stats.add_scan(sink_name, duration, has_errors: has_errors)
154
+ stats.add_scan(sink_name, sink_kind, duration, has_errors: has_errors)
155
155
  end
156
156
  end
157
157
 
@@ -160,12 +160,12 @@ module Aikido::Zen
160
160
  # @param attack [Aikido::Zen::Attack]
161
161
  # @return [void]
162
162
  def track_attack(attack)
163
- add_event(Events::TrackAttack.new(attack.sink.name, being_blocked: attack.blocked?))
163
+ add_event(Events::TrackAttack.new(attack.sink.name, attack.sink.kind, being_blocked: attack.blocked?))
164
164
  end
165
165
 
166
- def handle_track_attack(sink_name, being_blocked:)
166
+ def handle_track_attack(sink_name, sink_kind, being_blocked:)
167
167
  synchronize(@stats) do |stats|
168
- stats.add_attack(sink_name, being_blocked: being_blocked)
168
+ stats.add_attack(sink_name, sink_kind, being_blocked: being_blocked)
169
169
  end
170
170
  end
171
171
 
@@ -42,7 +42,7 @@ module Aikido::Zen
42
42
  attr_reader :api_endpoint
43
43
 
44
44
  # @return [URI] The HTTP host for the Aikido Runtime API. Defaults to
45
- # +https://runtime.aikido.dev+.
45
+ # +https://guard.aikido.dev+.
46
46
  attr_reader :realtime_endpoint
47
47
 
48
48
  # @return [Hash] HTTP timeouts for communicating with the API.
@@ -56,6 +56,10 @@ module Aikido::Zen
56
56
  # each initial heartbeat event.
57
57
  attr_accessor :initial_heartbeat_delays
58
58
 
59
+ # @return [Symbol] the agent mode for forked worker processes. Can be set
60
+ # through the AIKIDO_AGENT_MODE environment variable.
61
+ attr_reader :agent_mode
62
+
59
63
  # @return [Integer] the interval in seconds at which forked worker processes
60
64
  # poll the parent process for updated runtime settings. Defaults to 10 seconds.
61
65
  attr_accessor :worker_process_polling_interval
@@ -229,6 +233,11 @@ module Aikido::Zen
229
233
  attr_accessor :realtime_settings_updates_enabled
230
234
  alias_method :realtime_settings_updates_enabled?, :realtime_settings_updates_enabled
231
235
 
236
+ # @return [Integer] the minimum time in milliseconds between realtime
237
+ # settings updates.
238
+ # Defaults to 9 seconds in milliseconds.
239
+ attr_accessor :realtime_settings_updates_min_time_between_events
240
+
232
241
  def initialize
233
242
  self.insert_middleware_after = ::ActionDispatch::RemoteIp
234
243
  self.disabled = read_boolean_from_env(ENV.fetch("AIKIDO_DISABLE", false)) || read_boolean_from_env(ENV.fetch("AIKIDO_DISABLED", false))
@@ -236,10 +245,11 @@ module Aikido::Zen
236
245
  self.debugging = read_boolean_from_env(ENV.fetch("AIKIDO_DEBUG", false))
237
246
  self.api_token = ENV.fetch("AIKIDO_TOKEN", nil)
238
247
  self.api_endpoint = ENV.fetch("AIKIDO_ENDPOINT") { regional_api_endpoint(api_token) }
239
- self.realtime_endpoint = ENV.fetch("AIKIDO_REALTIME_ENDPOINT", DEFAULT_RUNTIME_BASE_URL)
248
+ self.realtime_endpoint = api_endpoint
240
249
  self.api_timeouts = 10
241
250
  self.polling_interval = 60 # 1 min
242
251
  self.initial_heartbeat_delays = [30, 60 * 2] # 30 sec, 2 min
252
+ self.agent_mode = ENV.fetch("AIKIDO_AGENT_MODE", "shared")
243
253
  self.worker_process_polling_interval = 10
244
254
  self.worker_process_polling_jitter = 10
245
255
  self.worker_process_heartbeat_interval = 10
@@ -277,6 +287,7 @@ module Aikido::Zen
277
287
  self.idor_excluded_table_names = []
278
288
  self.idor_max_cache_entries = 1000
279
289
  self.realtime_settings_updates_enabled = false
290
+ self.realtime_settings_updates_min_time_between_events = 9 * 1000 # 9 sec (ms)
280
291
  end
281
292
 
282
293
  # Set the base URL for API requests.
@@ -293,6 +304,17 @@ module Aikido::Zen
293
304
  @realtime_endpoint = URI(url)
294
305
  end
295
306
 
307
+ # @param mode [Symbol, String] a supported agent mode.
308
+ # @raise [ArgumentError]
309
+ def agent_mode=(mode)
310
+ mode = mode.to_sym
311
+ unless AGENT_MODES.include?(mode)
312
+ raise ArgumentError, "agent_mode must be one of #{AGENT_MODES.join(", ")}, got #{mode}"
313
+ end
314
+
315
+ @agent_mode = mode
316
+ end
317
+
296
318
  # Set the logger and configure its severity level according to agent's debug mode
297
319
  # @param logger [::Logger]
298
320
  def logger=(logger)
@@ -364,6 +386,9 @@ module Aikido::Zen
364
386
  end
365
387
  end
366
388
 
389
+ # @!visibility private
390
+ AGENT_MODES = [:shared, :per_worker].freeze
391
+
367
392
  # @!visibility private
368
393
  DEFAULT_AIKIDO_ENDPOINT = "https://guard.aikido.dev"
369
394
 
@@ -377,9 +402,6 @@ module Aikido::Zen
377
402
  "AU" => "https://guard.au.aikido.dev"
378
403
  }.freeze
379
404
 
380
- # @!visibility private
381
- DEFAULT_RUNTIME_BASE_URL = "https://runtime.aikido.dev"
382
-
383
405
  # @!visibility private
384
406
  DEFAULT_JSON_ENCODER = JSON.method(:dump)
385
407
 
@@ -95,5 +95,31 @@ module Aikido::Zen
95
95
  )
96
96
  end
97
97
  end
98
+
99
+ # Event sent by Aikido::Zen.track_custom_event to record a custom,
100
+ # user-named event happening during an HTTP request.
101
+ class Custom < Event
102
+ # @param name [String] the name of the tracked event.
103
+ # @param request [Aikido::Zen::Request]
104
+ # @param user [Aikido::Zen::Actor, nil]
105
+ # @param opts [Hash<Symbol, Object>] any other options to pass to
106
+ # the superclass initializer.
107
+ def initialize(name:, request:, user: nil, **opts)
108
+ super(type: "custom", **opts)
109
+ @name = name
110
+ @request = request
111
+ @user = user
112
+ end
113
+
114
+ def as_json
115
+ super.update(
116
+ {
117
+ name: @name,
118
+ request: @request.as_json,
119
+ user: @user && {id: @user.id, name: @user.name}.compact
120
+ }.compact
121
+ )
122
+ end
123
+ end
98
124
  end
99
125
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Aikido::Zen
4
- class RuntimeSettings::IPList
4
+ class Firewall::IPList
5
5
  attr_reader :key
6
6
  attr_reader :source
7
7
  attr_reader :description
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikido::Zen
4
+ class Firewall
5
+ # @return [Regexp, nil]
6
+ attr_accessor :blocked_user_agent_regexp
7
+
8
+ # @return [Regexp, nil]
9
+ attr_accessor :monitored_user_agent_regexp
10
+
11
+ # @return [Array<Hash>, nil]
12
+ attr_accessor :user_agent_details
13
+
14
+ # @return [Array<Aikido::Zen::Firewall::IPList>]
15
+ attr_accessor :blocked_ip_lists
16
+
17
+ # @return [Array<Aikido::Zen::Firewall::IPList>]
18
+ attr_accessor :allowed_ip_lists
19
+
20
+ # @return [Array<Aikido::Zen::Firewall::IPList>]
21
+ attr_accessor :monitored_ip_lists
22
+
23
+ def initialize
24
+ self.blocked_ip_lists = []
25
+ self.allowed_ip_lists = []
26
+ self.monitored_ip_lists = []
27
+ end
28
+
29
+ # @param data [Hash] the decoded JSON payload from /api/runtime/firewall/lists
30
+ # @return [Boolean]
31
+ def update_from_json(data)
32
+ self.blocked_user_agent_regexp = pattern(data["blockedUserAgents"])
33
+
34
+ self.monitored_user_agent_regexp = pattern(data["monitoredUserAgents"])
35
+
36
+ self.user_agent_details = []
37
+
38
+ data["userAgentDetails"]&.each do |record|
39
+ key = record["key"]
40
+ pattern = pattern(record["pattern"])
41
+
42
+ next if key.nil? || pattern.nil?
43
+
44
+ user_agent_details << {
45
+ key: key,
46
+ pattern: pattern
47
+ }
48
+ end
49
+
50
+ self.blocked_ip_lists = []
51
+
52
+ data["blockedIPAddresses"]&.each do |ip_list|
53
+ blocked_ip_lists << Firewall::IPList.from_json(ip_list)
54
+ end
55
+
56
+ self.allowed_ip_lists = []
57
+
58
+ data["allowedIPAddresses"]&.each do |ip_list|
59
+ allowed_ip_lists << Firewall::IPList.from_json(ip_list)
60
+ end
61
+
62
+ self.monitored_ip_lists = []
63
+
64
+ data["monitoredIPAddresses"]&.each do |ip_list|
65
+ monitored_ip_lists << Firewall::IPList.from_json(ip_list)
66
+ end
67
+
68
+ true
69
+ end
70
+
71
+ # Construct a regular expression from the non-nil and non-empty string,
72
+ # otherwise return nil.
73
+ #
74
+ # The resulting regular expression is case insensitive.
75
+ #
76
+ # @param string [String, nil]
77
+ # @return [Regexp, nil]
78
+ private def pattern(string)
79
+ return nil if string.nil? || string.empty?
80
+
81
+ begin
82
+ /#{string}/i
83
+ rescue RegexpError
84
+ nil
85
+ end
86
+ end
87
+
88
+ # @param user_agent [String] the user agent
89
+ # @return [Boolean] whether the user agent should be blocked
90
+ def blocked_user_agent?(user_agent)
91
+ return false if blocked_user_agent_regexp.nil?
92
+
93
+ blocked_user_agent_regexp.match?(user_agent)
94
+ end
95
+
96
+ # @param user_agent [String] the user agent
97
+ # @return [Boolean] whether the user agent should be monitored
98
+ def monitored_user_agent?(user_agent)
99
+ return false if monitored_user_agent_regexp.nil?
100
+
101
+ monitored_user_agent_regexp.match?(user_agent)
102
+ end
103
+
104
+ # @param user_agent [String] the user agent
105
+ # @return [Array<String>] the matching user agent keys
106
+ def user_agent_keys(user_agent)
107
+ return [] if user_agent_details.nil?
108
+
109
+ user_agent_details.filter_map { |record| record[:key] if record[:pattern].match?(user_agent) }
110
+ end
111
+
112
+ def allowed_ip?(ip)
113
+ allowed_ip_lists.empty? || allowed_ip_lists.any? { |ip_list| ip_list.include?(ip) }
114
+ end
115
+
116
+ def blocked_ip?(ip)
117
+ blocked_ip_lists.any? { |ip_list| ip_list.include?(ip) }
118
+ end
119
+
120
+ def monitored_ip?(ip)
121
+ monitored_ip_lists.any? { |ip_list| ip_list.include?(ip) }
122
+ end
123
+
124
+ def monitored_ip_list_keys(ip)
125
+ return [] if ip.nil?
126
+
127
+ monitored_ip_lists.filter_map { |ip_list| ip_list.key if ip_list.include?(ip) }
128
+ end
129
+ end
130
+ end
131
+
132
+ require_relative "firewall/ip_list"
@@ -11,14 +11,20 @@ module Aikido
11
11
  @app = app
12
12
 
13
13
  @pid = Concurrent::AtomicFixnum.new(Process.pid)
14
+ @fork_mutex = Mutex.new
14
15
  end
15
16
 
16
17
  def call(env)
17
- new_pid = Process.pid
18
- old_pid = @pid.value
18
+ pid = Process.pid
19
19
 
20
- if new_pid != old_pid && @pid.compare_and_set(old_pid, new_pid)
21
- Aikido::Zen.fork!
20
+ if pid != @pid.value
21
+ @fork_mutex.synchronize do
22
+ if pid != @pid.value
23
+ Aikido::Zen.fork!
24
+
25
+ @pid.value = pid
26
+ end
27
+ end
22
28
  end
23
29
 
24
30
  @app.call(env)