aikido-zen 0.1.1-x86_64-linux → 0.2.0-x86_64-linux

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/.simplecov +1 -0
  3. data/CHANGELOG.md +4 -0
  4. data/README.md +11 -2
  5. data/benchmarks/rails7.1_sql_injection.js +30 -34
  6. data/docs/banner.svg +128 -129
  7. data/docs/config.md +8 -6
  8. data/docs/rails.md +2 -2
  9. data/lib/aikido/zen/agent.rb +3 -1
  10. data/lib/aikido/zen/api_client.rb +3 -3
  11. data/lib/aikido/zen/attack.rb +105 -36
  12. data/lib/aikido/zen/collector/routes.rb +2 -0
  13. data/lib/aikido/zen/collector.rb +19 -3
  14. data/lib/aikido/zen/config.rb +44 -20
  15. data/lib/aikido/zen/errors.rb +10 -1
  16. data/lib/aikido/zen/event.rb +4 -2
  17. data/lib/aikido/zen/libzen-v0.1.37.x86_64.so +0 -0
  18. data/lib/aikido/zen/middleware/check_allowed_addresses.rb +2 -14
  19. data/lib/aikido/zen/middleware/middleware.rb +11 -0
  20. data/lib/aikido/zen/middleware/{throttler.rb → rack_throttler.rb} +3 -11
  21. data/lib/aikido/zen/middleware/request_tracker.rb +190 -0
  22. data/lib/aikido/zen/middleware/set_context.rb +1 -4
  23. data/lib/aikido/zen/payload.rb +2 -0
  24. data/lib/aikido/zen/rails_engine.rb +8 -0
  25. data/lib/aikido/zen/rate_limiter.rb +1 -1
  26. data/lib/aikido/zen/request/schema/builder.rb +0 -2
  27. data/lib/aikido/zen/request/schema/definition.rb +0 -5
  28. data/lib/aikido/zen/request/schema.rb +0 -3
  29. data/lib/aikido/zen/scanners/path_traversal/helpers.rb +65 -0
  30. data/lib/aikido/zen/scanners/path_traversal_scanner.rb +61 -0
  31. data/lib/aikido/zen/scanners/shell_injection/helpers.rb +159 -0
  32. data/lib/aikido/zen/scanners/shell_injection_scanner.rb +62 -0
  33. data/lib/aikido/zen/scanners/sql_injection_scanner.rb +0 -4
  34. data/lib/aikido/zen/scanners/ssrf_scanner.rb +9 -6
  35. data/lib/aikido/zen/scanners.rb +2 -0
  36. data/lib/aikido/zen/sinks/action_controller.rb +26 -12
  37. data/lib/aikido/zen/sinks/file.rb +120 -0
  38. data/lib/aikido/zen/sinks/kernel.rb +73 -0
  39. data/lib/aikido/zen/sinks.rb +8 -0
  40. data/lib/aikido/zen/system_info.rb +1 -1
  41. data/lib/aikido/zen/version.rb +2 -2
  42. data/lib/aikido/zen.rb +14 -1
  43. data/tasklib/bench.rake +3 -2
  44. metadata +16 -8
  45. data/lib/aikido/zen/libzen-v0.1.31.x86_64.so +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
@@ -40,7 +40,7 @@ You can just tell Zen to use it like so:
40
40
 
41
41
  ``` ruby
42
42
  # config/initializers/zen.rb
43
- Rails.application.config.zen.token = Rails.application.credentials.zen.token
43
+ Rails.application.config.zen.api_token = Rails.application.credentials.zen.token
44
44
  ```
45
45
 
46
46
  [creds]: https://guides.rubyonrails.org/security.html#environmental-security
@@ -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.
@@ -103,7 +103,9 @@ module Aikido::Zen
103
103
  def handle_attack(attack)
104
104
  attack.will_be_blocked! if @config.blocking_mode?
105
105
 
106
- @config.logger.error("[ATTACK DETECTED] #{attack.log_message}")
106
+ @config.logger.error(
107
+ format("Zen has %s a %s: %s", attack.blocked? ? "blocked" : "detected", attack.humanized_name, attack.as_json.to_json)
108
+ )
107
109
  report(Events::Attack.new(attack: attack)) if @api_client.can_make_requests?
108
110
 
109
111
  @collector.track_attack(attack)
@@ -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)
@@ -93,14 +93,14 @@ module Aikido::Zen
93
93
  # response.
94
94
  #
95
95
  # @param request [Net::HTTPRequest]
96
- # @param base_url [URI] which API to use. Defaults to +Config#api_base_url+.
96
+ # @param base_url [URI] which API to use. Defaults to +Config#api_endpoint+.
97
97
  #
98
98
  # @return [Object] the result of decoding the JSON response from the server.
99
99
  #
100
100
  # @raise [Aikido::Zen::APIError] in case of a 4XX or 5XX response.
101
101
  # @raise [Aikido::Zen::NetworkError] if an error occurs trying to make the
102
102
  # request.
103
- private def request(request, base_url: @config.api_base_url)
103
+ private def request(request, base_url: @config.api_endpoint)
104
104
  Net::HTTP.start(base_url.host, base_url.port, http_settings) do |http|
105
105
  response = http.request(request)
106
106
 
@@ -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
@@ -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,7 @@ 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
+ @middleware_installed = Concurrent::AtomicBoolean.new
14
15
  end
15
16
 
16
17
  # Flush all the stats into a Heartbeat event that can be reported back to
@@ -28,7 +29,9 @@ module Aikido::Zen
28
29
  start(at: at)
29
30
  stats = stats.flush(at: at)
30
31
 
31
- Events::Heartbeat.new(stats: stats, users: users, hosts: hosts, routes: routes)
32
+ Events::Heartbeat.new(
33
+ stats: stats, users: users, hosts: hosts, routes: routes, middleware_installed: middleware_installed?
34
+ )
32
35
  end
33
36
 
34
37
  # Sets the start time for this collection period.
@@ -39,13 +42,17 @@ module Aikido::Zen
39
42
  synchronize(@stats) { |stats| stats.start(at) }
40
43
  end
41
44
 
42
- # Track stats about the request, record the visited endpoint, and if
43
- # enabled, the API schema for this endpoint.
45
+ # Track stats about the requests
44
46
  #
45
47
  # @param request [Aikido::Zen::Request]
46
48
  # @return [void]
47
49
  def track_request(request)
48
50
  synchronize(@stats) { |stats| stats.add_request }
51
+ end
52
+
53
+ # Record the visited endpoint, and if enabled, the API schema for this endpoint.
54
+ # @param request [Aikido::Zen::Request]
55
+ def track_route(request)
49
56
  synchronize(@routes) { |routes| routes.add(request) if request.route }
50
57
  end
51
58
 
@@ -83,6 +90,10 @@ module Aikido::Zen
83
90
  synchronize(@users) { |users| users.add(actor) }
84
91
  end
85
92
 
93
+ def middleware_installed!
94
+ @middleware_installed.make_true
95
+ end
96
+
86
97
  # @api private
87
98
  def routes
88
99
  @routes.get
@@ -103,6 +114,11 @@ module Aikido::Zen
103
114
  @stats.get
104
115
  end
105
116
 
117
+ # @api private
118
+ def middleware_installed?
119
+ @middleware_installed.true?
120
+ end
121
+
106
122
  # Atomically modify an object's state within a block, ensuring it's safe
107
123
  # from other threads.
108
124
  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,11 @@ 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 [Boolean] is the agent in debugging mode?
59
+ attr_accessor :debugging
60
+ alias_method :debugging?, :debugging
57
61
 
58
62
  # @return [Integer] maximum number of timing measurements to keep in memory
59
63
  # before compressing them.
@@ -76,10 +80,10 @@ module Aikido::Zen
76
80
  # the oldest seen users.
77
81
  attr_accessor :max_users_tracked
78
82
 
79
- # @return [Proc{Aikido::Zen::Request => Array(Integer, Hash, #each)}]
80
- # Rack handler used to respond to requests from IPs blocked in the Aikido
83
+ # @return [Proc{(Aikido::Zen::Request, Symbol) => Array(Integer, Hash, #each)}]
84
+ # Rack handler used to respond to requests from IPs, users or others blocked in the Aikido
81
85
  # dashboard.
82
- attr_accessor :blocked_ip_responder
86
+ attr_accessor :blocked_responder
83
87
 
84
88
  # @return [Proc{Aikido::Zen::Request => Array(Integer, Hash, #each)}]
85
89
  # Rack handler used to respond to requests that have been rate limited.
@@ -90,6 +94,12 @@ module Aikido::Zen
90
94
  # differentiate different clients. By default this uses the request IP.
91
95
  attr_accessor :rate_limiting_discriminator
92
96
 
97
+ # @return [Boolean] whether Aikido Zen should collect api schemas.
98
+ # Defaults to true. Can be set through AIKIDO_FEATURE_COLLECT_API_SCHEMA
99
+ # environment variable.
100
+ attr_accessor :collect_api_schema
101
+ alias_method :collect_api_schema?, :collect_api_schema
102
+
93
103
  # @return [Integer] max number of requests we sample per endpoint when
94
104
  # computing the schema.
95
105
  attr_accessor :api_schema_max_samples
@@ -131,27 +141,29 @@ module Aikido::Zen
131
141
 
132
142
  def initialize
133
143
  self.disabled = read_boolean_from_env(ENV.fetch("AIKIDO_DISABLED", false))
134
- self.blocking_mode = read_boolean_from_env(ENV.fetch("AIKIDO_BLOCKING", false))
144
+ self.blocking_mode = read_boolean_from_env(ENV.fetch("AIKIDO_BLOCK", false))
135
145
  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)
146
+ self.api_endpoint = ENV.fetch("AIKIDO_ENDPOINT", DEFAULT_AIKIDO_ENDPOINT)
147
+ self.realtime_endpoint = ENV.fetch("AIKIDO_REALTIME_ENDPOINT", DEFAULT_RUNTIME_BASE_URL)
138
148
  self.api_token = ENV.fetch("AIKIDO_TOKEN", nil)
139
149
  self.polling_interval = 60
140
150
  self.initial_heartbeat_delay = 60
141
151
  self.json_encoder = DEFAULT_JSON_ENCODER
142
152
  self.json_decoder = DEFAULT_JSON_DECODER
143
- self.logger = Logger.new($stdout, progname: "aikido")
153
+ self.debugging = read_boolean_from_env(ENV.fetch("AIKIDO_DEBUG", false))
154
+ self.logger = Logger.new($stdout, progname: "aikido", level: debugging ? Logger::DEBUG : Logger::INFO)
144
155
  self.max_performance_samples = 5000
145
156
  self.max_compressed_stats = 100
146
157
  self.max_outbound_connections = 200
147
158
  self.max_users_tracked = 1000
148
159
  self.request_builder = Aikido::Zen::Context::RACK_REQUEST_BUILDER
149
- self.blocked_ip_responder = DEFAULT_BLOCKED_IP_RESPONDER
160
+ self.blocked_responder = DEFAULT_BLOCKED_RESPONDER
150
161
  self.rate_limited_responder = DEFAULT_RATE_LIMITED_RESPONDER
151
162
  self.rate_limiting_discriminator = DEFAULT_RATE_LIMITING_DISCRIMINATOR
152
163
  self.server_rate_limit_deadline = 1800 # 30 min
153
164
  self.client_rate_limit_period = 3600 # 1 hour
154
165
  self.client_rate_limit_max_events = 100
166
+ self.collect_api_schema = read_boolean_from_env(ENV.fetch("AIKIDO_FEATURE_COLLECT_API_SCHEMA", true))
155
167
  self.api_schema_max_samples = Integer(ENV.fetch("AIKIDO_MAX_API_DISCOVERY_SAMPLES", 10))
156
168
  self.api_schema_collection_max_depth = 20
157
169
  self.api_schema_collection_max_properties = 20
@@ -161,15 +173,22 @@ module Aikido::Zen
161
173
  # Set the base URL for API requests.
162
174
  #
163
175
  # @param url [String, URI]
164
- def api_base_url=(url)
165
- @api_base_url = URI(url)
176
+ def api_endpoint=(url)
177
+ @api_endpoint = URI(url)
166
178
  end
167
179
 
168
180
  # Set the base URL for runtime API requests.
169
181
  #
170
182
  # @param url [String, URI]
171
- def runtime_api_base_url=(url)
172
- @runtime_api_base_url = URI(url)
183
+ def realtime_endpoint=(url)
184
+ @realtime_endpoint = URI(url)
185
+ end
186
+
187
+ # Set the logger and configure its severity level according to agent's debug mode
188
+ # @param logger [::Logger]
189
+ def logger=(logger)
190
+ @logger = logger
191
+ @logger.level = Logger::DEBUG if debugging
173
192
  end
174
193
 
175
194
  # @overload def api_timeouts=(timeouts)
@@ -205,7 +224,7 @@ module Aikido::Zen
205
224
  end
206
225
 
207
226
  # @!visibility private
208
- DEFAULT_API_BASE_URL = "https://guard.aikido.dev"
227
+ DEFAULT_AIKIDO_ENDPOINT = "https://guard.aikido.dev"
209
228
 
210
229
  # @!visibility private
211
230
  DEFAULT_RUNTIME_BASE_URL = "https://runtime.aikido.dev"
@@ -217,9 +236,14 @@ module Aikido::Zen
217
236
  DEFAULT_JSON_DECODER = JSON.method(:parse)
218
237
 
219
238
  # @!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)]]
239
+ DEFAULT_BLOCKED_RESPONDER = ->(request, blocking_type) do
240
+ message = case blocking_type
241
+ when :ip
242
+ format("Your IP address is not allowed to access this resource. (Your IP: %s)", request.ip)
243
+ else
244
+ "You are blocked by Zen."
245
+ end
246
+ [403, {"Content-Type" => "text/plain"}, [message]]
223
247
  end
224
248
 
225
249
  # @!visibility private
@@ -60,7 +60,6 @@ module Aikido
60
60
  attr_reader :attack
61
61
 
62
62
  def initialize(attack)
63
- super(attack.log_message)
64
63
  @attack = attack
65
64
  end
66
65
  end
@@ -75,6 +74,16 @@ module Aikido
75
74
  def_delegators :@attack, :request, :input
76
75
  end
77
76
 
77
+ class PathTraversalError < UnderAttackError
78
+ extend Forwardable
79
+ def_delegators :@attack, :input
80
+ end
81
+
82
+ class ShellInjectionError < UnderAttackError
83
+ extend Forwardable
84
+ def_delegators :@attack, :input
85
+ end
86
+
78
87
  # Raised when there's any problem communicating (or loading) libzen.
79
88
  class InternalsError < ZenError
80
89
  # @param attempt [String] description of what we were trying to do.
@@ -48,12 +48,13 @@ module Aikido::Zen
48
48
  end
49
49
 
50
50
  class Heartbeat < Event
51
- def initialize(stats:, users:, hosts:, routes:, **opts)
51
+ def initialize(stats:, users:, hosts:, routes:, middleware_installed:, **opts)
52
52
  super(type: "heartbeat", **opts)
53
53
  @stats = stats
54
54
  @users = users
55
55
  @hosts = hosts
56
56
  @routes = routes
57
+ @middleware_installed = middleware_installed
57
58
  end
58
59
 
59
60
  def as_json
@@ -61,7 +62,8 @@ module Aikido::Zen
61
62
  stats: @stats.as_json,
62
63
  users: @users.as_json,
63
64
  routes: @routes.as_json,
64
- hostnames: @hosts.as_json
65
+ hostnames: @hosts.as_json,
66
+ middlewareInstalled: @middleware_installed
65
67
  )
66
68
  end
67
69
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../context"
4
-
5
3
  module Aikido::Zen
6
4
  module Middleware
7
5
  # Middleware that rejects requests from IPs blocked in the Aikido dashboard.
@@ -13,24 +11,14 @@ module Aikido::Zen
13
11
  end
14
12
 
15
13
  def call(env)
16
- request = request_from(env)
14
+ request = Aikido::Zen::Middleware.request_from(env)
17
15
 
18
16
  allowed_ips = @settings.endpoints[request.route].allowed_ips
19
17
 
20
18
  if allowed_ips.empty? || allowed_ips.include?(request.ip)
21
19
  @app.call(env)
22
20
  else
23
- @config.blocked_ip_responder.call(request)
24
- end
25
- end
26
-
27
- private
28
-
29
- def request_from(env)
30
- if (current_context = Aikido::Zen.current_context)
31
- current_context.request
32
- else
33
- Context.from_rack_env(env).request
21
+ @config.blocked_responder.call(request, :ip)
34
22
  end
35
23
  end
36
24
  end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikido::Zen::Middleware
4
+ def self.request_from(env)
5
+ if (current_context = Aikido::Zen.current_context)
6
+ current_context.request
7
+ else
8
+ Aikido::Zen::Context.from_rack_env(env).request
9
+ end
10
+ end
11
+ end
@@ -4,10 +4,10 @@ require_relative "../context"
4
4
 
5
5
  module Aikido::Zen
6
6
  module Middleware
7
- # Middleware that rejects requests from clients that are making too many
7
+ # Rack middleware that rejects requests from clients that are making too many
8
8
  # requests to a given endpoint, based in the runtime configuration in the
9
9
  # Aikido dashboard.
10
- class Throttler
10
+ class RackThrottler
11
11
  def initialize(
12
12
  app,
13
13
  config: Aikido::Zen.config,
@@ -21,7 +21,7 @@ module Aikido::Zen
21
21
  end
22
22
 
23
23
  def call(env)
24
- request = request_from(env)
24
+ request = Aikido::Zen::Middleware.request_from(env)
25
25
 
26
26
  if should_throttle?(request)
27
27
  @config.rate_limited_responder.call(request)
@@ -37,14 +37,6 @@ module Aikido::Zen
37
37
 
38
38
  @rate_limiter.throttle?(request)
39
39
  end
40
-
41
- def request_from(env)
42
- if (current_context = Aikido::Zen.current_context)
43
- current_context.request
44
- else
45
- Context.from_rack_env(env).request
46
- end
47
- end
48
40
  end
49
41
  end
50
42
  end