aikido-zen 0.1.1-x86_64-darwin → 1.0.0.pre.beta.1-x86_64-darwin

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 (62) hide show
  1. checksums.yaml +4 -4
  2. data/.simplecov +7 -0
  3. data/CHANGELOG.md +4 -0
  4. data/README.md +11 -2
  5. data/benchmarks/README.md +8 -12
  6. data/benchmarks/rails7.1_sql_injection.js +30 -34
  7. data/docs/banner.svg +128 -129
  8. data/docs/config.md +8 -6
  9. data/docs/rails.md +1 -1
  10. data/lib/aikido/zen/agent.rb +13 -9
  11. data/lib/aikido/zen/api_client.rb +17 -7
  12. data/lib/aikido/zen/attack.rb +105 -36
  13. data/lib/aikido/zen/background_worker.rb +52 -0
  14. data/lib/aikido/zen/collector/routes.rb +2 -0
  15. data/lib/aikido/zen/collector.rb +31 -4
  16. data/lib/aikido/zen/config.rb +55 -20
  17. data/lib/aikido/zen/detached_agent/agent.rb +78 -0
  18. data/lib/aikido/zen/detached_agent/front_object.rb +37 -0
  19. data/lib/aikido/zen/detached_agent/server.rb +41 -0
  20. data/lib/aikido/zen/detached_agent.rb +2 -0
  21. data/lib/aikido/zen/errors.rb +18 -1
  22. data/lib/aikido/zen/event.rb +4 -2
  23. data/lib/aikido/zen/libzen-v0.1.37.x86_64.dylib +0 -0
  24. data/lib/aikido/zen/middleware/check_allowed_addresses.rb +2 -14
  25. data/lib/aikido/zen/middleware/middleware.rb +11 -0
  26. data/lib/aikido/zen/middleware/{throttler.rb → rack_throttler.rb} +11 -13
  27. data/lib/aikido/zen/middleware/request_tracker.rb +190 -0
  28. data/lib/aikido/zen/middleware/set_context.rb +1 -4
  29. data/lib/aikido/zen/outbound_connection_monitor.rb +4 -0
  30. data/lib/aikido/zen/payload.rb +2 -0
  31. data/lib/aikido/zen/rails_engine.rb +12 -0
  32. data/lib/aikido/zen/rate_limiter/breaker.rb +3 -3
  33. data/lib/aikido/zen/rate_limiter.rb +7 -12
  34. data/lib/aikido/zen/request/rails_router.rb +6 -18
  35. data/lib/aikido/zen/request/schema/auth_schemas.rb +14 -0
  36. data/lib/aikido/zen/request/schema/builder.rb +0 -2
  37. data/lib/aikido/zen/request/schema/definition.rb +0 -5
  38. data/lib/aikido/zen/request/schema.rb +18 -3
  39. data/lib/aikido/zen/runtime_settings.rb +2 -2
  40. data/lib/aikido/zen/scanners/path_traversal/helpers.rb +65 -0
  41. data/lib/aikido/zen/scanners/path_traversal_scanner.rb +63 -0
  42. data/lib/aikido/zen/scanners/shell_injection/helpers.rb +159 -0
  43. data/lib/aikido/zen/scanners/shell_injection_scanner.rb +64 -0
  44. data/lib/aikido/zen/scanners/sql_injection_scanner.rb +4 -6
  45. data/lib/aikido/zen/scanners/ssrf/private_ip_checker.rb +33 -21
  46. data/lib/aikido/zen/scanners/ssrf_scanner.rb +15 -7
  47. data/lib/aikido/zen/scanners/stored_ssrf_scanner.rb +6 -0
  48. data/lib/aikido/zen/scanners.rb +2 -0
  49. data/lib/aikido/zen/sink.rb +6 -1
  50. data/lib/aikido/zen/sinks/action_controller.rb +34 -15
  51. data/lib/aikido/zen/sinks/file.rb +120 -0
  52. data/lib/aikido/zen/sinks/kernel.rb +73 -0
  53. data/lib/aikido/zen/sinks/socket.rb +13 -0
  54. data/lib/aikido/zen/sinks.rb +8 -0
  55. data/lib/aikido/zen/system_info.rb +1 -1
  56. data/lib/aikido/zen/version.rb +2 -2
  57. data/lib/aikido/zen/worker.rb +5 -0
  58. data/lib/aikido/zen.rb +54 -8
  59. data/tasklib/bench.rake +31 -7
  60. data/tasklib/wrk.rb +88 -0
  61. metadata +22 -8
  62. data/lib/aikido/zen/libzen-v0.1.31.x86_64.dylib +0 -0
data/docs/config.md CHANGED
@@ -17,7 +17,7 @@ requiring a full deploy.)
17
17
  ## Blocking mode
18
18
 
19
19
  In order to have Aikido block requests that look like attacks, you can set
20
- `AIKIDO_BLOCKING=true` in your environment, or set
20
+ `AIKIDO_BLOCK=true` in your environment, or set
21
21
  `Aikido::Zen.config.blocking_mode = true`.
22
22
 
23
23
  (We recommend the ENV variable as you can normally change this easily without
@@ -79,21 +79,23 @@ Aikido::Zen.rate_limited_responder = ->(request) {
79
79
  }
80
80
  ```
81
81
 
82
- ## IP Blocking responses
82
+ ## Blocking responses
83
83
 
84
- If you're using the IP blocking features of Zen, you can configure the response
84
+ If you're using the IP, users or bot blocking features of Zen, you can configure the response
85
85
  we send users when their request is rejected with a Proc that returns a
86
86
  Rack-compatible response tuple, like this:
87
87
 
88
88
  ``` ruby
89
- Aikido::Zen.blocked_ip_responder = ->(request) {
89
+ Aikido::Zen.blocked_responder = ->(request, blocking_type) {
90
90
  # Here, request is an instance of Aikido::Zen::Request, which follows the
91
91
  # underlying Rack::Request (or ActionDispatch::Request in Rails) API.
92
- [403, {"Content-Type" => "application/json"}, ['{"error":"ip_blocked"}']]
92
+ # And blocking_type is [:ip, :user]
93
+
94
+ [403, {"Content-Type" => "application/json"}, ['{"error":"#{blocking_type.to_s}_blocked"}']]
93
95
  }
94
96
  ```
95
97
 
96
- By default, Zen emits a `text/plain` 403 response that tells the user their IP
98
+ By default, Zen emits a `text/plain` 403 response that tells the user the request
97
99
  is not allowed.
98
100
 
99
101
  ## API schema sampling
data/docs/rails.md CHANGED
@@ -48,7 +48,7 @@ Rails.application.config.zen.token = Rails.application.credentials.zen.token
48
48
  ## Blocking mode
49
49
 
50
50
  By default, Zen will only detect and log attacks, but will not block them. You
51
- can enable blocking mode by setting the `AIKIDO_BLOCKING` environment variable
51
+ can enable blocking mode by setting the `AIKIDO_BLOCK` environment variable
52
52
  to `true`.
53
53
 
54
54
  When in blocking mode, Zen will raise an exception when it detects an attack.
@@ -19,6 +19,7 @@ module Aikido::Zen
19
19
  def initialize(
20
20
  config: Aikido::Zen.config,
21
21
  collector: Aikido::Zen.collector,
22
+ detached_agent: Aikido::Zen.detached_agent,
22
23
  worker: Aikido::Zen::Worker.new(config: config),
23
24
  api_client: Aikido::Zen::APIClient.new(config: config)
24
25
  )
@@ -28,6 +29,7 @@ module Aikido::Zen
28
29
  @worker = worker
29
30
  @api_client = api_client
30
31
  @collector = collector
32
+ @detached_agent = detached_agent
31
33
  end
32
34
 
33
35
  def started?
@@ -35,7 +37,7 @@ module Aikido::Zen
35
37
  end
36
38
 
37
39
  def start!
38
- @config.logger.info "Starting Aikido agent"
40
+ @config.logger.info "Starting Aikido agent v#{Aikido::Zen::VERSION}"
39
41
 
40
42
  raise Aikido::ZenError, "Aikido Agent already started!" if started?
41
43
  @started_at = Time.now.utc
@@ -57,7 +59,7 @@ module Aikido::Zen
57
59
  at_exit { stop! if started? }
58
60
 
59
61
  report(Events::Started.new(time: @started_at)) do |response|
60
- Aikido::Zen.runtime_settings.update_from_json(response)
62
+ updated_settings! if Aikido::Zen.runtime_settings.update_from_json(response)
61
63
  @config.logger.info "Updated runtime settings."
62
64
  rescue => err
63
65
  @config.logger.error(err.message)
@@ -103,7 +105,9 @@ module Aikido::Zen
103
105
  def handle_attack(attack)
104
106
  attack.will_be_blocked! if @config.blocking_mode?
105
107
 
106
- @config.logger.error("[ATTACK DETECTED] #{attack.log_message}")
108
+ @config.logger.error(
109
+ format("Zen has %s a %s: %s", attack.blocked? ? "blocked" : "detected", attack.humanized_name, attack.as_json.to_json)
110
+ )
107
111
  report(Events::Attack.new(attack: attack)) if @api_client.can_make_requests?
108
112
 
109
113
  @collector.track_attack(attack)
@@ -139,11 +143,11 @@ module Aikido::Zen
139
143
  def send_heartbeat(at: Time.now.utc)
140
144
  return unless @api_client.can_make_requests?
141
145
 
142
- event = @collector.flush(at: at)
143
-
144
- report(event) do |response|
145
- Aikido::Zen.runtime_settings.update_from_json(response)
146
- @config.logger.info "Updated runtime settings after heartbeat"
146
+ @collector.flush_heartbeats.each do |heartbeat|
147
+ report(heartbeat) do |response|
148
+ updated_settings! if Aikido::Zen.runtime_settings.update_from_json(response)
149
+ @config.logger.info "Updated runtime settings after heartbeat"
150
+ end
147
151
  end
148
152
  end
149
153
 
@@ -157,7 +161,7 @@ module Aikido::Zen
157
161
  def poll_for_setting_updates
158
162
  @worker.every(@config.polling_interval) do
159
163
  if @api_client.should_fetch_settings?
160
- Aikido::Zen.runtime_settings.update_from_json(@api_client.fetch_settings)
164
+ updated_settings! if Aikido::Zen.runtime_settings.update_from_json(@api_client.fetch_settings)
161
165
  @config.logger.info "Updated runtime settings after polling"
162
166
  end
163
167
  end
@@ -36,7 +36,7 @@ module Aikido::Zen
36
36
 
37
37
  response = request(
38
38
  Net::HTTP::Get.new("/config", default_headers),
39
- base_url: @config.runtime_api_base_url
39
+ base_url: @config.realtime_endpoint
40
40
  )
41
41
 
42
42
  new_updated_at = Time.at(response["configUpdatedAt"].to_i / 1000)
@@ -72,16 +72,26 @@ module Aikido::Zen
72
72
  # @return (see #fetch_settings)
73
73
  # @raise (see #request)
74
74
  def report(event)
75
- if @rate_limiter.throttle?(event)
76
- @config.logger.error("Not reporting #{event.type.upcase} event due to rate limiting")
75
+ event_type = if event.respond_to?(:type)
76
+ event.type
77
+ else
78
+ event[:type]
79
+ end
80
+
81
+ if @rate_limiter.throttle?(event_type)
82
+ @config.logger.error("Not reporting #{event_type.upcase} event due to rate limiting")
77
83
  return
78
84
  end
79
85
 
80
- @config.logger.debug("Reporting #{event.type.upcase} event")
86
+ @config.logger.debug("Reporting #{event_type.upcase} event")
81
87
 
82
88
  req = Net::HTTP::Post.new("/api/runtime/events", default_headers)
83
89
  req.content_type = "application/json"
84
- req.body = @config.json_encoder.call(event.as_json)
90
+ req.body = if event.respond_to?(:as_json)
91
+ @config.json_encoder.call(event.as_json)
92
+ else
93
+ @config.json_encoder.call(event)
94
+ end
85
95
 
86
96
  request(req)
87
97
  rescue Aikido::Zen::RateLimitedError
@@ -93,14 +103,14 @@ module Aikido::Zen
93
103
  # response.
94
104
  #
95
105
  # @param request [Net::HTTPRequest]
96
- # @param base_url [URI] which API to use. Defaults to +Config#api_base_url+.
106
+ # @param base_url [URI] which API to use. Defaults to +Config#api_endpoint+.
97
107
  #
98
108
  # @return [Object] the result of decoding the JSON response from the server.
99
109
  #
100
110
  # @raise [Aikido::Zen::APIError] in case of a 4XX or 5XX response.
101
111
  # @raise [Aikido::Zen::NetworkError] if an error occurs trying to make the
102
112
  # request.
103
- private def request(request, base_url: @config.api_base_url)
113
+ private def request(request, base_url: @config.api_endpoint)
104
114
  Net::HTTP.start(base_url.host, base_url.port, http_settings) do |http|
105
115
  response = http.request(request)
106
116
 
@@ -25,20 +25,93 @@ module Aikido::Zen
25
25
  @blocked
26
26
  end
27
27
 
28
- def log_message
28
+ def humanized_name
29
29
  raise NotImplementedError, "implement in subclasses"
30
30
  end
31
31
 
32
- def as_json
32
+ def kind
33
+ raise NotImplementedError, "implement in subclasses"
34
+ end
35
+
36
+ def input
37
+ raise NotImplementedError, "implement in subclasses"
38
+ end
39
+
40
+ def metadata
33
41
  raise NotImplementedError, "implement in subclasses"
34
42
  end
35
43
 
44
+ def as_json
45
+ {
46
+ kind: kind,
47
+ blocked: blocked?,
48
+ metadata: metadata,
49
+ operation: @operation
50
+ }.merge(input.as_json)
51
+ end
52
+
36
53
  def exception(*)
37
54
  raise NotImplementedError, "implement in subclasses"
38
55
  end
39
56
  end
40
57
 
41
58
  module Attacks
59
+ class PathTraversalAttack < Attack
60
+ attr_reader :input
61
+ attr_reader :filepath
62
+
63
+ def initialize(input:, filepath:, **opts)
64
+ super(**opts)
65
+ @input = input
66
+ @filepath = filepath
67
+ end
68
+
69
+ def metadata
70
+ {filename: filepath}
71
+ end
72
+
73
+ def humanized_name
74
+ "path traversal attack"
75
+ end
76
+
77
+ def kind
78
+ "path_traversal"
79
+ end
80
+
81
+ def exception(*)
82
+ PathTraversalError.new(self)
83
+ end
84
+ end
85
+
86
+ class ShellInjectionAttack < Attack
87
+ attr_reader :input
88
+ attr_reader :command
89
+
90
+ def initialize(input:, command:, **opts)
91
+ super(**opts)
92
+ @input = input
93
+ @command = command
94
+ end
95
+
96
+ def humanized_name
97
+ "shell injection"
98
+ end
99
+
100
+ def kind
101
+ "shell_injection"
102
+ end
103
+
104
+ def metadata
105
+ {
106
+ command: @command
107
+ }
108
+ end
109
+
110
+ def exception(*)
111
+ ShellInjectionError.new(self)
112
+ end
113
+ end
114
+
42
115
  class SQLInjectionAttack < Attack
43
116
  attr_reader :query
44
117
  attr_reader :input
@@ -51,20 +124,16 @@ module Aikido::Zen
51
124
  @dialect = dialect
52
125
  end
53
126
 
54
- def log_message
55
- format(
56
- "SQL Injection: Malicious user input «%s» detected in %s query «%s»",
57
- @input, @dialect, @query
58
- )
127
+ def humanized_name
128
+ "SQL injection"
59
129
  end
60
130
 
61
- def as_json
62
- {
63
- kind: "sql_injection",
64
- blocked: blocked?,
65
- metadata: {sql: @query},
66
- operation: @operation
67
- }.merge(@input.as_json)
131
+ def kind
132
+ "sql_injection"
133
+ end
134
+
135
+ def metadata
136
+ {sql: @query}
68
137
  end
69
138
 
70
139
  def exception(*)
@@ -82,24 +151,23 @@ module Aikido::Zen
82
151
  @request = request
83
152
  end
84
153
 
85
- def log_message
86
- format(
87
- "SSRF: Request to user-supplied hostname «%s» detected in %s (%s).",
88
- @input, @operation, @request
89
- ).strip
154
+ def humanized_name
155
+ "server-side request forgery"
156
+ end
157
+
158
+ def kind
159
+ "ssrf"
90
160
  end
91
161
 
92
162
  def exception(*)
93
163
  SSRFDetectedError.new(self)
94
164
  end
95
165
 
96
- def as_json
166
+ def metadata
97
167
  {
98
- kind: "ssrf",
99
- metadata: {host: @request.uri.hostname, port: @request.uri.port},
100
- blocked: blocked?,
101
- operation: @operation
102
- }.merge(@input.as_json)
168
+ host: @request.uri.hostname,
169
+ port: @request.uri.port
170
+ }
103
171
  end
104
172
  end
105
173
 
@@ -115,23 +183,24 @@ module Aikido::Zen
115
183
  @address = address
116
184
  end
117
185
 
118
- def log_message
119
- format(
120
- "Stored SSRF: Request to sensitive host «%s» (%s) detected from unknown source in %s",
121
- @hostname, @address, @operation
122
- )
186
+ def humanized_name
187
+ "server-side request forgery"
123
188
  end
124
189
 
125
190
  def exception(*)
126
191
  SSRFDetectedError.new(self)
127
192
  end
128
193
 
129
- def as_json
130
- {
131
- kind: "ssrf",
132
- blocked: blocked?,
133
- operation: @operation
134
- }
194
+ def kind
195
+ "ssrf"
196
+ end
197
+
198
+ def input
199
+ Aikido::Zen::Payload::UNKNOWN_PAYLOAD
200
+ end
201
+
202
+ def metadata
203
+ {}
135
204
  end
136
205
  end
137
206
  end
@@ -0,0 +1,52 @@
1
+ module Aikido::Zen
2
+ # Generic background worker class backed by queue. Meant to be used by any
3
+ # background process that needs to do heavy tasks.
4
+ class BackgroundWorker
5
+ # @param block [block] A block that receives 1 message directly from the queue
6
+ def initialize(&block)
7
+ @queue = Queue.new
8
+ @block = block
9
+ end
10
+
11
+ # starts the background thread, blocking the thread until a new messages arrives
12
+ # or the queue is stopped.
13
+ def start
14
+ @thread = Thread.new do
15
+ while running? || actions?
16
+ action = wait_for_action
17
+ @block.call(action) unless action.nil?
18
+ end
19
+ end
20
+ end
21
+
22
+ def restart
23
+ stop
24
+ @queue = Queue.new # re-open the queue
25
+ start
26
+ end
27
+
28
+ # Drain the queue to do not lose any messages
29
+ def stop
30
+ @queue.close # stop accepting messages
31
+ @thread.join # wait for the queue to be drained
32
+ end
33
+
34
+ def enqueue(scan)
35
+ @queue.push(scan)
36
+ end
37
+
38
+ private
39
+
40
+ def actions?
41
+ !@queue.empty?
42
+ end
43
+
44
+ def running?
45
+ !@queue.closed?
46
+ end
47
+
48
+ def wait_for_action
49
+ @queue.pop(false)
50
+ end
51
+ end
52
+ end
@@ -7,6 +7,8 @@ module Aikido::Zen
7
7
  #
8
8
  # Keeps track of the visited routes.
9
9
  class Collector::Routes
10
+ attr_reader :visits
11
+
10
12
  def initialize(config = Aikido::Zen.config)
11
13
  @config = config
12
14
  @visits = Hash.new { |h, k| h[k] = Record.new }
@@ -11,6 +11,8 @@ module Aikido::Zen
11
11
  @users = Concurrent::AtomicReference.new(Users.new(@config))
12
12
  @hosts = Concurrent::AtomicReference.new(Hosts.new(@config))
13
13
  @routes = Concurrent::AtomicReference.new(Routes.new(@config))
14
+ @heartbeats = Queue.new
15
+ @middleware_installed = Concurrent::AtomicBoolean.new
14
16
  end
15
17
 
16
18
  # Flush all the stats into a Heartbeat event that can be reported back to
@@ -28,7 +30,19 @@ module Aikido::Zen
28
30
  start(at: at)
29
31
  stats = stats.flush(at: at)
30
32
 
31
- Events::Heartbeat.new(stats: stats, users: users, hosts: hosts, routes: routes)
33
+ Events::Heartbeat.new(
34
+ stats: stats, users: users, hosts: hosts, routes: routes, middleware_installed: middleware_installed?
35
+ )
36
+ end
37
+
38
+ # Put heartbeats coming from child processes into the internal queue.
39
+ def push_heartbeat(heartbeat)
40
+ @heartbeats << heartbeat
41
+ end
42
+
43
+ # Drains into an array all the queued heartbeats
44
+ def flush_heartbeats
45
+ Array.new(@heartbeats.size) { @heartbeats.pop }
32
46
  end
33
47
 
34
48
  # Sets the start time for this collection period.
@@ -39,13 +53,17 @@ module Aikido::Zen
39
53
  synchronize(@stats) { |stats| stats.start(at) }
40
54
  end
41
55
 
42
- # Track stats about the request, record the visited endpoint, and if
43
- # enabled, the API schema for this endpoint.
56
+ # Track stats about the requests
44
57
  #
45
58
  # @param request [Aikido::Zen::Request]
46
59
  # @return [void]
47
- def track_request(request)
60
+ def track_request(*)
48
61
  synchronize(@stats) { |stats| stats.add_request }
62
+ end
63
+
64
+ # Record the visited endpoint, and if enabled, the API schema for this endpoint.
65
+ # @param request [Aikido::Zen::Request]
66
+ def track_route(request)
49
67
  synchronize(@routes) { |routes| routes.add(request) if request.route }
50
68
  end
51
69
 
@@ -83,6 +101,10 @@ module Aikido::Zen
83
101
  synchronize(@users) { |users| users.add(actor) }
84
102
  end
85
103
 
104
+ def middleware_installed!
105
+ @middleware_installed.make_true
106
+ end
107
+
86
108
  # @api private
87
109
  def routes
88
110
  @routes.get
@@ -103,6 +125,11 @@ module Aikido::Zen
103
125
  @stats.get
104
126
  end
105
127
 
128
+ # @api private
129
+ def middleware_installed?
130
+ @middleware_installed.true?
131
+ end
132
+
106
133
  # Atomically modify an object's state within a block, ensuring it's safe
107
134
  # from other threads.
108
135
  private def synchronize(object)
@@ -16,18 +16,18 @@ module Aikido::Zen
16
16
  alias_method :disabled?, :disabled
17
17
 
18
18
  # @return [Boolean] whether Aikido should only report infractions or block
19
- # the request by raising an Exception. Defaults to whether AIKIDO_BLOCKING
19
+ # the request by raising an Exception. Defaults to whether AIKIDO_BLOCK
20
20
  # is set to a non-empty value in your environment, or +false+ otherwise.
21
21
  attr_accessor :blocking_mode
22
22
  alias_method :blocking_mode?, :blocking_mode
23
23
 
24
24
  # @return [URI] The HTTP host for the Aikido API. Defaults to
25
25
  # +https://guard.aikido.dev+.
26
- attr_reader :api_base_url
26
+ attr_reader :api_endpoint
27
27
 
28
28
  # @return [URI] The HTTP host for the Aikido Runtime API. Defaults to
29
29
  # +https://runtime.aikido.dev+.
30
- attr_reader :runtime_api_base_url
30
+ attr_reader :realtime_endpoint
31
31
 
32
32
  # @return [Hash] HTTP timeouts for communicating with the API.
33
33
  attr_reader :api_timeouts
@@ -53,7 +53,16 @@ module Aikido::Zen
53
53
  attr_accessor :json_decoder
54
54
 
55
55
  # @return [Logger]
56
- attr_accessor :logger
56
+ attr_reader :logger
57
+
58
+ # @return [string] Path of the socket where the detached agent will listen.
59
+ # By default, is stored under the root application path with file name
60
+ # `aikido-detached-agent.socket`
61
+ attr_reader :detached_agent_socket_path
62
+
63
+ # @return [Boolean] is the agent in debugging mode?
64
+ attr_accessor :debugging
65
+ alias_method :debugging?, :debugging
57
66
 
58
67
  # @return [Integer] maximum number of timing measurements to keep in memory
59
68
  # before compressing them.
@@ -76,10 +85,10 @@ module Aikido::Zen
76
85
  # the oldest seen users.
77
86
  attr_accessor :max_users_tracked
78
87
 
79
- # @return [Proc{Aikido::Zen::Request => Array(Integer, Hash, #each)}]
80
- # Rack handler used to respond to requests from IPs blocked in the Aikido
88
+ # @return [Proc{(Aikido::Zen::Request, Symbol) => Array(Integer, Hash, #each)}]
89
+ # Rack handler used to respond to requests from IPs, users or others blocked in the Aikido
81
90
  # dashboard.
82
- attr_accessor :blocked_ip_responder
91
+ attr_accessor :blocked_responder
83
92
 
84
93
  # @return [Proc{Aikido::Zen::Request => Array(Integer, Hash, #each)}]
85
94
  # Rack handler used to respond to requests that have been rate limited.
@@ -90,6 +99,12 @@ module Aikido::Zen
90
99
  # differentiate different clients. By default this uses the request IP.
91
100
  attr_accessor :rate_limiting_discriminator
92
101
 
102
+ # @return [Boolean] whether Aikido Zen should collect api schemas.
103
+ # Defaults to true. Can be set through AIKIDO_FEATURE_COLLECT_API_SCHEMA
104
+ # environment variable.
105
+ attr_accessor :collect_api_schema
106
+ alias_method :collect_api_schema?, :collect_api_schema
107
+
93
108
  # @return [Integer] max number of requests we sample per endpoint when
94
109
  # computing the schema.
95
110
  attr_accessor :api_schema_max_samples
@@ -131,27 +146,30 @@ module Aikido::Zen
131
146
 
132
147
  def initialize
133
148
  self.disabled = read_boolean_from_env(ENV.fetch("AIKIDO_DISABLED", false))
134
- self.blocking_mode = read_boolean_from_env(ENV.fetch("AIKIDO_BLOCKING", false))
149
+ self.blocking_mode = read_boolean_from_env(ENV.fetch("AIKIDO_BLOCK", false))
135
150
  self.api_timeouts = 10
136
- self.api_base_url = ENV.fetch("AIKIDO_BASE_URL", DEFAULT_API_BASE_URL)
137
- self.runtime_api_base_url = ENV.fetch("AIKIDO_RUNTIME_URL", DEFAULT_RUNTIME_BASE_URL)
151
+ self.api_endpoint = ENV.fetch("AIKIDO_ENDPOINT", DEFAULT_AIKIDO_ENDPOINT)
152
+ self.realtime_endpoint = ENV.fetch("AIKIDO_REALTIME_ENDPOINT", DEFAULT_RUNTIME_BASE_URL)
138
153
  self.api_token = ENV.fetch("AIKIDO_TOKEN", nil)
139
154
  self.polling_interval = 60
140
155
  self.initial_heartbeat_delay = 60
141
156
  self.json_encoder = DEFAULT_JSON_ENCODER
142
157
  self.json_decoder = DEFAULT_JSON_DECODER
143
- self.logger = Logger.new($stdout, progname: "aikido")
158
+ self.debugging = read_boolean_from_env(ENV.fetch("AIKIDO_DEBUG", false))
159
+ self.logger = Logger.new($stdout, progname: "aikido", level: debugging ? Logger::DEBUG : Logger::INFO)
144
160
  self.max_performance_samples = 5000
161
+ self.detached_agent_socket_path = "aikido-detached-agent.socket"
145
162
  self.max_compressed_stats = 100
146
163
  self.max_outbound_connections = 200
147
164
  self.max_users_tracked = 1000
148
165
  self.request_builder = Aikido::Zen::Context::RACK_REQUEST_BUILDER
149
- self.blocked_ip_responder = DEFAULT_BLOCKED_IP_RESPONDER
166
+ self.blocked_responder = DEFAULT_BLOCKED_RESPONDER
150
167
  self.rate_limited_responder = DEFAULT_RATE_LIMITED_RESPONDER
151
168
  self.rate_limiting_discriminator = DEFAULT_RATE_LIMITING_DISCRIMINATOR
152
169
  self.server_rate_limit_deadline = 1800 # 30 min
153
170
  self.client_rate_limit_period = 3600 # 1 hour
154
171
  self.client_rate_limit_max_events = 100
172
+ self.collect_api_schema = read_boolean_from_env(ENV.fetch("AIKIDO_FEATURE_COLLECT_API_SCHEMA", true))
155
173
  self.api_schema_max_samples = Integer(ENV.fetch("AIKIDO_MAX_API_DISCOVERY_SAMPLES", 10))
156
174
  self.api_schema_collection_max_depth = 20
157
175
  self.api_schema_collection_max_properties = 20
@@ -161,15 +179,22 @@ module Aikido::Zen
161
179
  # Set the base URL for API requests.
162
180
  #
163
181
  # @param url [String, URI]
164
- def api_base_url=(url)
165
- @api_base_url = URI(url)
182
+ def api_endpoint=(url)
183
+ @api_endpoint = URI(url)
166
184
  end
167
185
 
168
186
  # Set the base URL for runtime API requests.
169
187
  #
170
188
  # @param url [String, URI]
171
- def runtime_api_base_url=(url)
172
- @runtime_api_base_url = URI(url)
189
+ def realtime_endpoint=(url)
190
+ @realtime_endpoint = URI(url)
191
+ end
192
+
193
+ # Set the logger and configure its severity level according to agent's debug mode
194
+ # @param logger [::Logger]
195
+ def logger=(logger)
196
+ @logger = logger
197
+ @logger.level = Logger::DEBUG if debugging
173
198
  end
174
199
 
175
200
  # @overload def api_timeouts=(timeouts)
@@ -191,6 +216,11 @@ module Aikido::Zen
191
216
  @api_timeouts.update(value)
192
217
  end
193
218
 
219
+ def detached_agent_socket_path=(path)
220
+ @detached_agent_socket_path = path
221
+ @detached_agent_socket_path = "drbunix:" + @detached_agent_socket_path unless @detached_agent_socket_path.start_with?("drbunix:")
222
+ end
223
+
194
224
  private
195
225
 
196
226
  def read_boolean_from_env(value)
@@ -205,7 +235,7 @@ module Aikido::Zen
205
235
  end
206
236
 
207
237
  # @!visibility private
208
- DEFAULT_API_BASE_URL = "https://guard.aikido.dev"
238
+ DEFAULT_AIKIDO_ENDPOINT = "https://guard.aikido.dev"
209
239
 
210
240
  # @!visibility private
211
241
  DEFAULT_RUNTIME_BASE_URL = "https://runtime.aikido.dev"
@@ -217,9 +247,14 @@ module Aikido::Zen
217
247
  DEFAULT_JSON_DECODER = JSON.method(:parse)
218
248
 
219
249
  # @!visibility private
220
- DEFAULT_BLOCKED_IP_RESPONDER = ->(request) do
221
- message = "Your IP address is not allowed to access this resource. (Your IP: %s)"
222
- [403, {"Content-Type" => "text/plain"}, [format(message, request.ip)]]
250
+ DEFAULT_BLOCKED_RESPONDER = ->(request, blocking_type) do
251
+ message = case blocking_type
252
+ when :ip
253
+ format("Your IP address is not allowed to access this resource. (Your IP: %s)", request.ip)
254
+ else
255
+ "You are blocked by Zen."
256
+ end
257
+ [403, {"Content-Type" => "text/plain"}, [message]]
223
258
  end
224
259
 
225
260
  # @!visibility private