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
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "drb/drb"
4
+ require "drb/unix"
5
+ require_relative "front_object"
6
+ require_relative "../background_worker"
7
+
8
+ module Aikido::Zen::DetachedAgent
9
+ # Agent that runs in forked processes. It communicates with the parent process to dRB
10
+ # calls. It's in charge of schedule and send heartbeats to the *parent process*, to be
11
+ # later pushed.
12
+ #
13
+ # heartbeat & polling interval are configured to 10s , because they are connecting with
14
+ # parent process. We want to have the freshest data.
15
+ #
16
+ # It's possible to use `extend Forwardable` here for one-line forward calls to the
17
+ # @detached_agent_front object. Unfortunately, the methods to be called are
18
+ # created at runtime by `DRbObject`, which leads to an ugly warning about
19
+ # private methods after the delegator is bound.
20
+ class Agent
21
+ attr_reader :worker
22
+
23
+ def initialize(
24
+ heartbeat_interval: 10,
25
+ polling_interval: 10,
26
+ config: Aikido::Zen.config,
27
+ collector: Aikido::Zen.collector,
28
+ worker: Aikido::Zen::Worker.new(config: config)
29
+ )
30
+ @config = config
31
+ @heartbeat_interval = heartbeat_interval
32
+ @polling_interval = polling_interval
33
+ @worker = worker
34
+ @collector = collector
35
+ @detached_agent_front = DRbObject.new_with_uri(config.detached_agent_socket_path)
36
+ @has_forked = false
37
+ schedule_tasks
38
+ end
39
+
40
+ def send_heartbeat(at: Time.now.utc)
41
+ return unless @collector.stats.any?
42
+
43
+ heartbeat = @collector.flush(at: at)
44
+ @detached_agent_front.send_heartbeat_to_parent_process(heartbeat.as_json)
45
+ end
46
+
47
+ private def schedule_tasks
48
+ # For heartbeats is correct to send them from parent or child process. Otherwise, we'll lose
49
+ # stats made by the parent process.
50
+ @worker.every(@heartbeat_interval, run_now: false) { send_heartbeat }
51
+
52
+ # Runtime_settings fetch must happens only in the child processes, otherwise, due to
53
+ # we are updating the global runtime_settings, we could have an infinite recursion.
54
+ if @has_forked
55
+ @worker.every(@polling_interval) do
56
+ Aikido::Zen.runtime_settings = @detached_agent_front.updated_settings
57
+ @config.logger.debug "Updated runtime settings after polling from child process #{Process.pid}"
58
+ end
59
+ end
60
+ end
61
+
62
+ def calculate_rate_limits(request)
63
+ @detached_agent_front.calculate_rate_limits(request.route, request.ip, request.actor.to_json)
64
+ end
65
+
66
+ # Every time a fork occurs (a new child process is created), we need to start
67
+ # a DRb service in a background thread within the child process. This service
68
+ # will manage the connection and handle resource cleanup.
69
+ def handle_fork
70
+ @has_forked = true
71
+ DRb.start_service
72
+ # we need to ensure that there are not more jobs in the queue, but
73
+ # we reuse the same object
74
+ @worker.restart
75
+ schedule_tasks
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ # dRB Front object that will work as a bridge communication between child & parent
4
+ # processes.
5
+ # Every method is called from the child but it runs in the parent process.
6
+ module Aikido::Zen::DetachedAgent
7
+ class FrontObject
8
+ def initialize(
9
+ config: Aikido::Zen.config,
10
+ collector: Aikido::Zen.collector,
11
+ runtime_settings: Aikido::Zen.runtime_settings,
12
+ rate_limiter: Aikido::Zen::RateLimiter.new
13
+ )
14
+ @config = config
15
+ @collector = collector
16
+ @rate_limiter = rate_limiter
17
+ @runtime_settings = runtime_settings
18
+ end
19
+
20
+ RequestKind = Struct.new(:route, :schema, :ip, :actor)
21
+
22
+ def send_heartbeat_to_parent_process(heartbeat)
23
+ @collector.push_heartbeat(heartbeat)
24
+ end
25
+
26
+ # Method called by child processes to get an up-to-date version of the
27
+ # runtime_settings
28
+ def updated_settings
29
+ @runtime_settings
30
+ end
31
+
32
+ def calculate_rate_limits(route, ip, actor_hash)
33
+ actor = Aikido::Zen::Actor(actor_hash) if actor_hash
34
+ @rate_limiter.calculate_rate_limits(RequestKind.new(route, nil, ip, actor))
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikido::Zen::DetachedAgent
4
+ class Server
5
+ def initialize(config: Aikido::Zen.config)
6
+ @detached_agent_front = FrontObject.new
7
+ @drb_server = DRb.start_service(config.detached_agent_socket_path, @detached_agent_front)
8
+
9
+ # We don't want to see drb logs unless in debug mode
10
+ @drb_server.verbose = config.logger.debug?
11
+ end
12
+
13
+ def alive?
14
+ @drb_server.alive?
15
+ end
16
+
17
+ def stop!
18
+ @drb_server.stop_service
19
+ DRb.stop_service
20
+ end
21
+
22
+ class << self
23
+ def start!
24
+ Aikido::Zen.config.logger.debug("Starting DRb Server...")
25
+ max_attempts = 10
26
+ @server = new
27
+
28
+ attempts = 0
29
+ until @server.alive?
30
+ Aikido::Zen.config.logger.info("DRb Server still not alive. #{max_attempts - attempts} attempts remaining")
31
+ sleep 0.1
32
+ attempts += 1
33
+ raise Aikido::Zen::DetachedAgentError.new("Impossible to start the dRB server (socket=#{Aikido::Zen.config.detached_agent_socket_path})") \
34
+ if attempts == max_attempts
35
+ end
36
+
37
+ @server
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,2 @@
1
+ require_relative "detached_agent/agent"
2
+ require_relative "detached_agent/server"
@@ -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.
@@ -86,5 +95,13 @@ module Aikido
86
95
  MSG
87
96
  end
88
97
  end
98
+
99
+ class DetachedAgentError < ZenError
100
+ extend Forwardable
101
+
102
+ def initialize(msg)
103
+ super
104
+ end
105
+ end
89
106
  end
90
107
  end
@@ -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,24 +4,24 @@ 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,
14
14
  settings: Aikido::Zen.runtime_settings,
15
- rate_limiter: Aikido::Zen::RateLimiter.new
15
+ detached_agent: Aikido::Zen.detached_agent
16
16
  )
17
17
  @app = app
18
18
  @config = config
19
19
  @settings = settings
20
- @rate_limiter = rate_limiter
20
+ @detached_agent = detached_agent
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)
@@ -33,17 +33,15 @@ module Aikido::Zen
33
33
  private
34
34
 
35
35
  def should_throttle?(request)
36
+ return false unless @settings.endpoints[request.route].rate_limiting.enabled?
36
37
  return false if @settings.skip_protection_for_ips.include?(request.ip)
37
38
 
38
- @rate_limiter.throttle?(request)
39
- end
39
+ result = @detached_agent.calculate_rate_limits(request)
40
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
41
+ return false unless result
42
+
43
+ request.env["aikido.rate_limiting"] = result
44
+ request.env["aikido.rate_limiting"].throttled?
47
45
  end
48
46
  end
49
47
  end
@@ -0,0 +1,190 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikido::Zen
4
+ module Middleware
5
+ # Rack middleware used to track request
6
+ # It implements the logic under that which is considered worthy of being tracked.
7
+ class RequestTracker
8
+ def initialize(app)
9
+ @app = app
10
+ end
11
+
12
+ def call(env)
13
+ request = Aikido::Zen::Middleware.request_from(env)
14
+ response = @app.call(env)
15
+
16
+ Aikido::Zen.track_request request
17
+
18
+ if Aikido::Zen.config.collect_api_schema? && request.route && track?(
19
+ status_code: response[0],
20
+ route: request.route.path,
21
+ http_method: request.request_method
22
+ )
23
+ Aikido::Zen.track_discovered_route(request)
24
+ end
25
+
26
+ response
27
+ end
28
+
29
+ IGNORED_METHODS = %w[OPTIONS HEAD]
30
+ IGNORED_EXTENSIONS = %w[properties config webmanifest]
31
+ IGNORED_SEGMENTS = ["cgi-bin"]
32
+ WELL_KNOWN_URIS = %w[
33
+ /.well-known/acme-challenge
34
+ /.well-known/amphtml
35
+ /.well-known/api-catalog
36
+ /.well-known/appspecific
37
+ /.well-known/ashrae
38
+ /.well-known/assetlinks.json
39
+ /.well-known/broadband-labels
40
+ /.well-known/brski
41
+ /.well-known/caldav
42
+ /.well-known/carddav
43
+ /.well-known/change-password
44
+ /.well-known/cmp
45
+ /.well-known/coap
46
+ /.well-known/coap-eap
47
+ /.well-known/core
48
+ /.well-known/csaf
49
+ /.well-known/csaf-aggregator
50
+ /.well-known/csvm
51
+ /.well-known/did.json
52
+ /.well-known/did-configuration.json
53
+ /.well-known/dnt
54
+ /.well-known/dnt-policy.txt
55
+ /.well-known/dots
56
+ /.well-known/ecips
57
+ /.well-known/edhoc
58
+ /.well-known/enterprise-network-security
59
+ /.well-known/enterprise-transport-security
60
+ /.well-known/est
61
+ /.well-known/genid
62
+ /.well-known/gnap-as-rs
63
+ /.well-known/gpc.json
64
+ /.well-known/gs1resolver
65
+ /.well-known/hoba
66
+ /.well-known/host-meta
67
+ /.well-known/host-meta.json
68
+ /.well-known/hosting-provider
69
+ /.well-known/http-opportunistic
70
+ /.well-known/idp-proxy
71
+ /.well-known/jmap
72
+ /.well-known/keybase.txt
73
+ /.well-known/knx
74
+ /.well-known/looking-glass
75
+ /.well-known/masque
76
+ /.well-known/matrix
77
+ /.well-known/mercure
78
+ /.well-known/mta-sts.txt
79
+ /.well-known/mud
80
+ /.well-known/nfv-oauth-server-configuration
81
+ /.well-known/ni
82
+ /.well-known/nodeinfo
83
+ /.well-known/nostr.json
84
+ /.well-known/oauth-authorization-server
85
+ /.well-known/oauth-protected-resource
86
+ /.well-known/ohttp-gateway
87
+ /.well-known/openid-federation
88
+ /.well-known/open-resource-discovery
89
+ /.well-known/openid-configuration
90
+ /.well-known/openorg
91
+ /.well-known/oslc
92
+ /.well-known/pki-validation
93
+ /.well-known/posh
94
+ /.well-known/privacy-sandbox-attestations.json
95
+ /.well-known/private-token-issuer-directory
96
+ /.well-known/probing.txt
97
+ /.well-known/pvd
98
+ /.well-known/rd
99
+ /.well-known/related-website-set.json
100
+ /.well-known/reload-config
101
+ /.well-known/repute-template
102
+ /.well-known/resourcesync
103
+ /.well-known/sbom
104
+ /.well-known/security.txt
105
+ /.well-known/ssf-configuration
106
+ /.well-known/sshfp
107
+ /.well-known/stun-key
108
+ /.well-known/terraform.json
109
+ /.well-known/thread
110
+ /.well-known/time
111
+ /.well-known/timezone
112
+ /.well-known/tdmrep.json
113
+ /.well-known/tor-relay
114
+ /.well-known/tpcd
115
+ /.well-known/traffic-advice
116
+ /.well-known/trust.txt
117
+ /.well-known/uma2-configuration
118
+ /.well-known/void
119
+ /.well-known/webfinger
120
+ /.well-known/webweaver.json
121
+ /.well-known/wot
122
+ ]
123
+
124
+ # @param status_code [Integer]
125
+ # @param route [String]
126
+ # @param http_method [String]
127
+ def track?(status_code:, route:, http_method:)
128
+ # In the UI we want to show only successful (2xx) or redirect (3xx) responses
129
+ # anything else is discarded.
130
+ return false unless status_code >= 200 && status_code <= 399
131
+
132
+ return false if IGNORED_METHODS.include?(http_method)
133
+
134
+ segments = route.split "/"
135
+
136
+ # Do not discover routes with dot files like `/path/to/.file` or `/.directory/file`
137
+ # We want to allow discovery of well-known URIs like `/.well-known/acme-challenge`
138
+ return false if segments.any? { |s| is_dot_file s } && !is_well_known_uri(route)
139
+
140
+ return false if segments.any? { |s| contains_ignored_string s }
141
+
142
+ # Check for every file segment if it contains a file extension and if it
143
+ # should be discovered or ignored
144
+ segments.all? { |s| should_track_extension s }
145
+ end
146
+
147
+ private
148
+
149
+ # Check if a path is a well-known URI
150
+ # e.g. /.well-known/acme-challenge
151
+ # https://www.iana.org/assignments/well-known-uris/well-known-uris.xhtml
152
+ def is_well_known_uri(route)
153
+ WELL_KNOWN_URIS.include?(route)
154
+ end
155
+
156
+ def is_dot_file(segment)
157
+ segment.start_with?(".") && segment.size > 1
158
+ end
159
+
160
+ def contains_ignored_string(segment)
161
+ IGNORED_SEGMENTS.any? { |ignored| segment.include?(ignored) }
162
+ end
163
+
164
+ # Ignore routes which contain file extensions
165
+ def should_track_extension(segment)
166
+ extension = get_file_extension(segment)
167
+
168
+ return true unless extension
169
+
170
+ # Do not discover files with extensions of 1 to 5 characters,
171
+ # e.g. file.css, file.js, file.woff2
172
+ return false if extension.size > 1 && extension.size < 6
173
+
174
+ # Ignore some file extensions that are longer than 5 characters or shorter than 2 chars
175
+ return false if IGNORED_EXTENSIONS.include?(extension)
176
+
177
+ true
178
+ end
179
+
180
+ def get_file_extension(segment)
181
+ extension = File.extname(segment)
182
+ if extension&.start_with?(".")
183
+ # Remove the dot from the extension
184
+ return extension[1..]
185
+ end
186
+ extension
187
+ end
188
+ end
189
+ end
190
+ end
@@ -15,10 +15,7 @@ module Aikido::Zen
15
15
  end
16
16
 
17
17
  def call(env)
18
- context = Context.from_rack_env(env)
19
-
20
- Aikido::Zen.current_context = env[ENV_KEY] = context
21
- Aikido::Zen.track_request(context.request)
18
+ Aikido::Zen.current_context = env[ENV_KEY] = Context.from_rack_env(env)
22
19
 
23
20
  @app.call(env)
24
21
  ensure
@@ -5,6 +5,10 @@ module Aikido::Zen
5
5
  # any Sink that wraps an HTTP library, and lets us keep track of any hosts to
6
6
  # which the app communicates over HTTP.
7
7
  module OutboundConnectionMonitor
8
+ def self.skips_on_nil_context?
9
+ false
10
+ end
11
+
8
12
  # This simply reports the connection to the Agent, and always returns +nil+
9
13
  # as it's not scanning for any particular attack.
10
14
  #
@@ -12,6 +12,8 @@ module Aikido::Zen
12
12
  @path = path
13
13
  end
14
14
 
15
+ UNKNOWN_PAYLOAD = Payload.new("unknown", "unknown", "unknown")
16
+
15
17
  alias_method :to_s, :value
16
18
 
17
19
  def ==(other)
@@ -14,6 +14,9 @@ module Aikido::Zen
14
14
 
15
15
  app.middleware.use Aikido::Zen::Middleware::SetContext
16
16
  app.middleware.use Aikido::Zen::Middleware::CheckAllowedAddresses
17
+ # Request Tracker stats do not consider failed request or 40x, so the middleware
18
+ # must be the last one wrapping the request.
19
+ app.middleware.use Aikido::Zen::Middleware::RequestTracker
17
20
 
18
21
  ActiveSupport.on_load(:action_controller) do
19
22
  # Due to how Rails sets up its middleware chain, the routing is evaluated
@@ -57,6 +60,15 @@ module Aikido::Zen
57
60
  # that any gems required after aikido-zen are detected and patched
58
61
  # accordingly.
59
62
  Aikido::Zen.load_sinks!
63
+
64
+ # It's important we start after loading sinks, so we can report the installed packages
65
+ Aikido::Zen.start!
66
+ Aikido::Zen.start!
67
+
68
+ # Agent's bootstrap process has finished —Controllers are patched to block
69
+ # unwanted requests, sinks are loaded, scanners are running—, so we mark
70
+ # the agent as installed.
71
+ Aikido::Zen.middleware_installed!
60
72
  end
61
73
  end
62
74
  end
@@ -31,13 +31,13 @@ module Aikido::Zen
31
31
  @opened_at = @clock.call
32
32
  end
33
33
 
34
- # @param event [#type] an event which we'll discriminate by type to decide
34
+ # @param event_type [String] an event type which we'll use to decide
35
35
  # if we should throttle it.
36
36
  # @return [Boolean]
37
- def throttle?(event)
37
+ def throttle?(event_type)
38
38
  return true if open? && !try_close
39
39
 
40
- result = @bucket.increment(event.type)
40
+ result = @bucket.increment(event_type)
41
41
  result.throttled?
42
42
  end
43
43
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "synchronizable"
4
- require_relative "middleware/throttler"
4
+ require_relative "middleware/rack_throttler"
5
5
 
6
6
  module Aikido::Zen
7
7
  # Keeps track of all requests in this process, broken up by Route and further
@@ -24,23 +24,18 @@ module Aikido::Zen
24
24
  }
25
25
  end
26
26
 
27
- # Checks whether the request requires rate limiting. As a side effect, this
28
- # will annotate the request with the "aikido.rate_limiting" ENV key, holding
29
- # the result of the check, and including useful stats in case you want to
30
- # return RateLimit headers..
27
+ # Calculate based on the configuration whether a request will be
28
+ # rate-limited or not.
31
29
  #
32
30
  # @param request [Aikido::Zen::Request]
33
- # @return [Boolean]
34
- #
35
- # @see Aikido::Zen::RateLimiter::Result
36
- def throttle?(request)
31
+ # @return [Aikido::Zen::RateLimiter::Result, nil]
32
+ def calculate_rate_limits(request)
37
33
  settings = settings_for(request.route)
38
- return false unless settings.enabled?
34
+ return nil unless settings.enabled?
39
35
 
40
36
  bucket = @buckets[request.route]
41
37
  key = @config.rate_limiting_discriminator.call(request)
42
- request.env["aikido.rate_limiting"] = bucket.increment(key)
43
- request.env["aikido.rate_limiting"].throttled?
38
+ bucket.increment(key)
44
39
  end
45
40
 
46
41
  private
@@ -58,27 +58,15 @@ module Aikido::Zen
58
58
  end
59
59
 
60
60
  private def build_route(route, request, prefix: request.script_name)
61
- Rails::Route.new(route, prefix: prefix, verb: request.request_method)
62
- end
63
- end
64
-
65
- module Rails
66
- class Route < Aikido::Zen::Route
67
- attr_reader :verb
61
+ route_wrapper = ActionDispatch::Routing::RouteWrapper.new(route)
68
62
 
69
- def initialize(rails_route, verb: rails_route.verb, prefix: nil)
70
- @route = ActionDispatch::Routing::RouteWrapper.new(rails_route)
71
- @verb = verb
72
- @prefix = prefix
63
+ path = if prefix.present?
64
+ File.join(prefix.to_s, route_wrapper.path).chomp("/")
65
+ else
66
+ route_wrapper.path
73
67
  end
74
68
 
75
- def path
76
- if @prefix.present?
77
- File.join(@prefix.to_s, @route.path).chomp("/")
78
- else
79
- @route.path
80
- end
81
- end
69
+ Aikido::Zen::Route.new(verb: request.request_method, path: path)
82
70
  end
83
71
  end
84
72
  end