aikido-zen 1.0.2-aarch64-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 (125) hide show
  1. checksums.yaml +7 -0
  2. data/.aikido +6 -0
  3. data/.ruby-version +1 -0
  4. data/.simplecov +32 -0
  5. data/.standard.yml +3 -0
  6. data/LICENSE +674 -0
  7. data/README.md +148 -0
  8. data/Rakefile +67 -0
  9. data/benchmarks/README.md +22 -0
  10. data/benchmarks/rails7.1_benchmark.js +1 -0
  11. data/benchmarks/rails7.1_sql_injection.js +102 -0
  12. data/docs/banner.svg +202 -0
  13. data/docs/config.md +133 -0
  14. data/docs/proxy.md +10 -0
  15. data/docs/rails.md +112 -0
  16. data/docs/troubleshooting.md +62 -0
  17. data/lib/aikido/zen/actor.rb +146 -0
  18. data/lib/aikido/zen/agent/heartbeats_manager.rb +66 -0
  19. data/lib/aikido/zen/agent.rb +181 -0
  20. data/lib/aikido/zen/api_client.rb +145 -0
  21. data/lib/aikido/zen/attack.rb +217 -0
  22. data/lib/aikido/zen/attack_wave/helpers.rb +457 -0
  23. data/lib/aikido/zen/attack_wave.rb +88 -0
  24. data/lib/aikido/zen/background_worker.rb +52 -0
  25. data/lib/aikido/zen/cache.rb +91 -0
  26. data/lib/aikido/zen/capped_collections.rb +86 -0
  27. data/lib/aikido/zen/collector/event.rb +238 -0
  28. data/lib/aikido/zen/collector/hosts.rb +30 -0
  29. data/lib/aikido/zen/collector/routes.rb +71 -0
  30. data/lib/aikido/zen/collector/sink_stats.rb +95 -0
  31. data/lib/aikido/zen/collector/stats.rb +122 -0
  32. data/lib/aikido/zen/collector/users.rb +32 -0
  33. data/lib/aikido/zen/collector.rb +223 -0
  34. data/lib/aikido/zen/config.rb +312 -0
  35. data/lib/aikido/zen/context/rack_request.rb +27 -0
  36. data/lib/aikido/zen/context/rails_request.rb +47 -0
  37. data/lib/aikido/zen/context.rb +145 -0
  38. data/lib/aikido/zen/detached_agent/agent.rb +79 -0
  39. data/lib/aikido/zen/detached_agent/front_object.rb +41 -0
  40. data/lib/aikido/zen/detached_agent/server.rb +78 -0
  41. data/lib/aikido/zen/detached_agent.rb +2 -0
  42. data/lib/aikido/zen/errors.rb +107 -0
  43. data/lib/aikido/zen/event.rb +116 -0
  44. data/lib/aikido/zen/helpers.rb +24 -0
  45. data/lib/aikido/zen/internals.rb +123 -0
  46. data/lib/aikido/zen/libzen-v0.1.48-aarch64-linux.so +0 -0
  47. data/lib/aikido/zen/middleware/allowed_address_checker.rb +26 -0
  48. data/lib/aikido/zen/middleware/attack_wave_protector.rb +46 -0
  49. data/lib/aikido/zen/middleware/context_setter.rb +26 -0
  50. data/lib/aikido/zen/middleware/fork_detector.rb +23 -0
  51. data/lib/aikido/zen/middleware/middleware.rb +11 -0
  52. data/lib/aikido/zen/middleware/rack_throttler.rb +50 -0
  53. data/lib/aikido/zen/middleware/request_tracker.rb +197 -0
  54. data/lib/aikido/zen/outbound_connection.rb +62 -0
  55. data/lib/aikido/zen/outbound_connection_monitor.rb +23 -0
  56. data/lib/aikido/zen/package.rb +22 -0
  57. data/lib/aikido/zen/payload.rb +50 -0
  58. data/lib/aikido/zen/rails_engine.rb +53 -0
  59. data/lib/aikido/zen/rate_limiter/breaker.rb +61 -0
  60. data/lib/aikido/zen/rate_limiter/bucket.rb +76 -0
  61. data/lib/aikido/zen/rate_limiter/result.rb +31 -0
  62. data/lib/aikido/zen/rate_limiter.rb +50 -0
  63. data/lib/aikido/zen/request/heuristic_router.rb +115 -0
  64. data/lib/aikido/zen/request/rails_router.rb +92 -0
  65. data/lib/aikido/zen/request/schema/auth_discovery.rb +86 -0
  66. data/lib/aikido/zen/request/schema/auth_schemas.rb +54 -0
  67. data/lib/aikido/zen/request/schema/builder.rb +121 -0
  68. data/lib/aikido/zen/request/schema/definition.rb +107 -0
  69. data/lib/aikido/zen/request/schema/empty_schema.rb +28 -0
  70. data/lib/aikido/zen/request/schema.rb +87 -0
  71. data/lib/aikido/zen/request.rb +88 -0
  72. data/lib/aikido/zen/route.rb +96 -0
  73. data/lib/aikido/zen/runtime_settings/endpoints.rb +78 -0
  74. data/lib/aikido/zen/runtime_settings/ip_set.rb +36 -0
  75. data/lib/aikido/zen/runtime_settings/protection_settings.rb +62 -0
  76. data/lib/aikido/zen/runtime_settings/rate_limit_settings.rb +47 -0
  77. data/lib/aikido/zen/runtime_settings.rb +66 -0
  78. data/lib/aikido/zen/scan.rb +75 -0
  79. data/lib/aikido/zen/scanners/path_traversal/helpers.rb +68 -0
  80. data/lib/aikido/zen/scanners/path_traversal_scanner.rb +64 -0
  81. data/lib/aikido/zen/scanners/shell_injection/helpers.rb +159 -0
  82. data/lib/aikido/zen/scanners/shell_injection_scanner.rb +65 -0
  83. data/lib/aikido/zen/scanners/sql_injection_scanner.rb +94 -0
  84. data/lib/aikido/zen/scanners/ssrf/dns_lookups.rb +27 -0
  85. data/lib/aikido/zen/scanners/ssrf/private_ip_checker.rb +97 -0
  86. data/lib/aikido/zen/scanners/ssrf_scanner.rb +266 -0
  87. data/lib/aikido/zen/scanners/stored_ssrf_scanner.rb +55 -0
  88. data/lib/aikido/zen/scanners.rb +7 -0
  89. data/lib/aikido/zen/sink.rb +118 -0
  90. data/lib/aikido/zen/sinks/action_controller.rb +85 -0
  91. data/lib/aikido/zen/sinks/async_http.rb +80 -0
  92. data/lib/aikido/zen/sinks/curb.rb +113 -0
  93. data/lib/aikido/zen/sinks/em_http.rb +83 -0
  94. data/lib/aikido/zen/sinks/excon.rb +118 -0
  95. data/lib/aikido/zen/sinks/file.rb +153 -0
  96. data/lib/aikido/zen/sinks/http.rb +93 -0
  97. data/lib/aikido/zen/sinks/httpclient.rb +95 -0
  98. data/lib/aikido/zen/sinks/httpx.rb +78 -0
  99. data/lib/aikido/zen/sinks/kernel.rb +33 -0
  100. data/lib/aikido/zen/sinks/mysql2.rb +31 -0
  101. data/lib/aikido/zen/sinks/net_http.rb +101 -0
  102. data/lib/aikido/zen/sinks/patron.rb +103 -0
  103. data/lib/aikido/zen/sinks/pg.rb +72 -0
  104. data/lib/aikido/zen/sinks/resolv.rb +62 -0
  105. data/lib/aikido/zen/sinks/socket.rb +85 -0
  106. data/lib/aikido/zen/sinks/sqlite3.rb +46 -0
  107. data/lib/aikido/zen/sinks/trilogy.rb +31 -0
  108. data/lib/aikido/zen/sinks/typhoeus.rb +78 -0
  109. data/lib/aikido/zen/sinks.rb +36 -0
  110. data/lib/aikido/zen/sinks_dsl.rb +250 -0
  111. data/lib/aikido/zen/synchronizable.rb +24 -0
  112. data/lib/aikido/zen/system_info.rb +80 -0
  113. data/lib/aikido/zen/version.rb +10 -0
  114. data/lib/aikido/zen/worker.rb +87 -0
  115. data/lib/aikido/zen.rb +303 -0
  116. data/lib/aikido-zen.rb +3 -0
  117. data/placeholder/.gitignore +4 -0
  118. data/placeholder/README.md +11 -0
  119. data/placeholder/Rakefile +75 -0
  120. data/placeholder/lib/placeholder.rb.template +3 -0
  121. data/placeholder/placeholder.gemspec.template +20 -0
  122. data/tasklib/bench.rake +94 -0
  123. data/tasklib/libzen.rake +133 -0
  124. data/tasklib/wrk.rb +88 -0
  125. metadata +214 -0
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module Aikido::Zen::DetachedAgent
6
+ class Server
7
+ # Initialize and start a detached agent server instance.
8
+ #
9
+ # @return [Aikido::Zen::DetachedAgent::Server]
10
+ def self.start(**opts)
11
+ new(**opts).tap(&:start!)
12
+ end
13
+
14
+ def initialize(config: Aikido::Zen.config)
15
+ @started_at = nil
16
+
17
+ @config = config
18
+
19
+ @socket_path = config.detached_agent_socket_path
20
+ @socket_uri = config.detached_agent_socket_uri
21
+ end
22
+
23
+ def started?
24
+ !!@started_at
25
+ end
26
+
27
+ def start!
28
+ @config.logger.info("Starting DRb Server...")
29
+
30
+ # Try to ensure that the DRb service can start if the DRb service did
31
+ # not stop cleanly.
32
+ begin
33
+ # Check whether the Unix domain socket is in use by another process.
34
+ UNIXSocket.new(@socket_path).close
35
+ rescue Errno::ECONNREFUSED
36
+ @config.logger.debug("Removing residual Unix domain socket...")
37
+
38
+ # Remove the residual Unix domain socket.
39
+ FileUtils.rm_f(@socket_path)
40
+ rescue
41
+ # empty
42
+ end
43
+
44
+ @front = FrontObject.new
45
+
46
+ # If the Unix domain socket is in use by another process and/or the
47
+ # residual Unix domain socket could not be removed DRb will raise an
48
+ # appropriate error.
49
+ @drb_server = DRb.start_service(@socket_uri, @front)
50
+
51
+ # Only show DRb output in debug mode.
52
+ @drb_server.verbose = @config.logger.debug?
53
+
54
+ # Ensure that the DRb server is alive.
55
+ max_attempts = 10
56
+ attempts = 0
57
+ until @drb_server.alive?
58
+ @config.logger.info("DRb Server still not alive. #{max_attempts - attempts} attempts remaining")
59
+ sleep 0.1
60
+ attempts += 1
61
+ raise Aikido::Zen::DetachedAgentError.new("Impossible to start the dRB server (socket=#{Aikido::Zen.config.detached_agent_socket_path})") \
62
+ if attempts == max_attempts
63
+ end
64
+
65
+ @started_at = Time.now.utc
66
+
67
+ at_exit { stop! if started? }
68
+ end
69
+
70
+ def stop!
71
+ @config.logger.info("Stopping DRb Server...")
72
+ @started_at = nil
73
+
74
+ @drb_server.stop_service if @drb_server.alive?
75
+ DRb.stop_service
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,2 @@
1
+ require_relative "detached_agent/agent"
2
+ require_relative "detached_agent/server"
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ module Aikido
6
+ # @!visibility private
7
+ # Support rescuing Aikido::Error without forcing a single base class to all
8
+ # errors (so things that should be e.g. a TypeError, can have the correct
9
+ # superclass).
10
+ module Error; end # :nodoc:
11
+
12
+ # @!visibility private
13
+ # Generic error for problems with the Agent.
14
+ class ZenError < RuntimeError
15
+ include Error
16
+ end
17
+
18
+ module Zen
19
+ # Wrapper for all low-level network errors communicating with the API. You
20
+ # can access the original error by calling #cause.
21
+ class NetworkError < StandardError
22
+ include Error
23
+
24
+ def initialize(request, cause = nil)
25
+ @request = request.dup
26
+
27
+ super("Error in #{request.method} #{request.path}: #{cause.message}")
28
+ end
29
+ end
30
+
31
+ # Raised whenever a request to the API results in a 4XX or 5XX response.
32
+ class APIError < StandardError
33
+ include Error
34
+
35
+ attr_reader :request
36
+ attr_reader :response
37
+
38
+ def initialize(request, response)
39
+ @request = anonimize_token(request.dup)
40
+ @response = response
41
+
42
+ super("Error in #{request.method} #{request.path}: #{response.code} #{response.message} (#{response.body})")
43
+ end
44
+
45
+ private def anonimize_token(request)
46
+ # Anonimize the token to `********************xxxx`,
47
+ # mimicking what we show in the dashbaord.
48
+ request["Authorization"] = request["Authorization"].to_s
49
+ .gsub(/\A.*(.{4})\z/, ("*" * 20) + "\\1")
50
+ request
51
+ end
52
+ end
53
+
54
+ # Raised whenever a response to the API results in a 429 response.
55
+ class RateLimitedError < APIError; end
56
+
57
+ class UnderAttackError < StandardError
58
+ include Error
59
+
60
+ attr_reader :attack
61
+
62
+ def initialize(attack)
63
+ @attack = attack
64
+ end
65
+ end
66
+
67
+ class SQLInjectionError < UnderAttackError
68
+ extend Forwardable
69
+ def_delegators :@attack, :query, :input, :dialect
70
+ end
71
+
72
+ class SSRFDetectedError < UnderAttackError
73
+ extend Forwardable
74
+ def_delegators :@attack, :request, :input
75
+ end
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
+
87
+ # Raised when there's any problem communicating (or loading) libzen.
88
+ class InternalsError < ZenError
89
+ # @param attempt [String] description of what we were trying to do.
90
+ # @param problem [String] what couldn't be done.
91
+ # @param libname [String] the name of the file (including the arch).
92
+ def initialize(attempt, problem, libname)
93
+ super(format(<<~MSG.chomp, attempt, problem, libname))
94
+ Zen could not scan %s due to a problem %s the library `%s'
95
+ MSG
96
+ end
97
+ end
98
+
99
+ class DetachedAgentError < ZenError
100
+ extend Forwardable
101
+
102
+ def initialize(msg)
103
+ super
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikido::Zen
4
+ # Base class for all events. You should be using one of the subclasses defined
5
+ # in the Events module.
6
+ class Event
7
+ attr_reader :type
8
+ attr_reader :time
9
+ attr_reader :system_info
10
+
11
+ def initialize(type:, system_info: Aikido::Zen.system_info, time: Time.now.utc)
12
+ @type = type
13
+ @time = time
14
+ @system_info = system_info
15
+ end
16
+
17
+ def as_json
18
+ {
19
+ type: type,
20
+ time: time.to_i * 1000,
21
+ agent: system_info.as_json
22
+ }
23
+ end
24
+ end
25
+
26
+ module Events
27
+ # Event sent when starting up the agent.
28
+ class Started < Event
29
+ def initialize(**opts)
30
+ super(type: "started", **opts)
31
+ end
32
+ end
33
+
34
+ class Attack < Event
35
+ attr_reader :attack
36
+
37
+ def initialize(attack:, **opts)
38
+ @attack = attack
39
+ super(type: "detected_attack", **opts)
40
+ end
41
+
42
+ def as_json
43
+ super.update(
44
+ {
45
+ attack: @attack.as_json,
46
+ request: @attack.context&.request&.as_json
47
+ }.compact
48
+ )
49
+ end
50
+ end
51
+
52
+ class Heartbeat < Event
53
+ def initialize(stats:, users:, hosts:, routes:, middleware_installed:, **opts)
54
+ super(type: "heartbeat", **opts)
55
+ @stats = stats
56
+ @users = users
57
+ @hosts = hosts
58
+ @routes = routes
59
+ @middleware_installed = middleware_installed
60
+ end
61
+
62
+ def as_json
63
+ super.update(
64
+ stats: @stats.as_json,
65
+ users: @users.as_json,
66
+ routes: @routes.as_json,
67
+ hostnames: @hosts.as_json,
68
+ middlewareInstalled: @middleware_installed
69
+ )
70
+ end
71
+ end
72
+
73
+ class AttackWave < Event
74
+ # @param [Aikido::Zen::Context] a context
75
+ # @return [Aikido::Zen::Events::AttackWave] an attack wave event
76
+ def self.from_context(context)
77
+ request = Aikido::Zen::AttackWave::Request.new(
78
+ ip_address: context.request.client_ip,
79
+ user_agent: context.request.user_agent,
80
+ source: context.request.framework
81
+ )
82
+
83
+ attack = Aikido::Zen::AttackWave::Attack.new(
84
+ metadata: {}, # not used yet
85
+ user: context.request.actor
86
+ )
87
+
88
+ new(request: request, attack: attack)
89
+ end
90
+
91
+ # @return [Aikido::Zen::AttackWave::Request]
92
+ attr_reader :request
93
+
94
+ # @return [Aikido::Zen::AttackWave::Attack]
95
+ attr_reader :attack
96
+
97
+ # @param [Aikido::Zen::AttackWave::Request] the attack wave request
98
+ # @param [Aikido::Zen::AttackWave::Attack] the attack wave attack
99
+ # @param opts [Hash<Symbol, Object>] any other options to pass to
100
+ # the superclass initializer.
101
+ # @return [Aikido::Zen::Events::AttackWave] an attack wave event
102
+ def initialize(request:, attack:, **opts)
103
+ super(type: "detected_attack_wave", **opts)
104
+ @request = request
105
+ @attack = attack
106
+ end
107
+
108
+ def as_json
109
+ super.update(
110
+ request: @request.as_json,
111
+ attack: @attack.as_json
112
+ )
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikido
4
+ module Zen
5
+ # @api private
6
+ module Helpers
7
+ # Normalizes a path by:
8
+ #
9
+ # 1. Collapsing consecutive forward slashes into a single forward slash.
10
+ # 2. Removing forward trailing slash, unless the normalized path is "/".
11
+ #
12
+ # @param path [String, nil] the path to normalize.
13
+ # @return [String, nil] the normalized path.
14
+ def self.normalize_path(path)
15
+ return path unless path
16
+
17
+ normalized_path = path.dup
18
+ normalized_path.squeeze!("/")
19
+ normalized_path.chomp!("/") unless normalized_path == "/"
20
+ normalized_path
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ffi"
4
+ require_relative "errors"
5
+
6
+ module Aikido::Zen
7
+ module Internals
8
+ extend FFI::Library
9
+
10
+ def self.libzen_names
11
+ lib_name = "libzen-v#{LIBZEN_VERSION}"
12
+ lib_ext = FFI::Platform::LIBSUFFIX
13
+
14
+ # Gem::Platform#version should be understood as an arbitrary Ruby defined
15
+ # OS specific string. A platform with a version string is considered more
16
+ # specific than a platform without a version string.
17
+ # https://docs.ruby-lang.org/en/3.3/Gem/Platform.html
18
+
19
+ platform = Gem::Platform.local.dup
20
+
21
+ # Library names in preferred order.
22
+ #
23
+ # If two library names are added, the specific platform library names is
24
+ # first and the generic platform library name is second.
25
+ names = []
26
+
27
+ names << "#{lib_name}-#{platform}.#{lib_ext}"
28
+
29
+ unless platform.version.nil?
30
+ platform.version = nil
31
+ names << "#{lib_name}-#{platform}.#{lib_ext}"
32
+ end
33
+
34
+ names
35
+ end
36
+
37
+ # @return [String] the name of the extension we're loading, which we can
38
+ # use in error messages.
39
+ def self.libzen_name
40
+ # The most generic platform library name.
41
+ libzen_names.last
42
+ end
43
+
44
+ # Load the most specific library
45
+ def self.load_libzen
46
+ libzen_names.each do |name|
47
+ path = File.expand_path(name, __dir__)
48
+ begin
49
+ return ffi_lib(path)
50
+ rescue LoadError
51
+ # empty
52
+ end
53
+ end
54
+ raise LoadError, "Zen could not load its native extension #{libzen_name}"
55
+ end
56
+
57
+ begin
58
+ load_libzen
59
+
60
+ # @!method self.detect_sql_injection_native(query, input, dialect)
61
+ # @param (see .detect_sql_injection)
62
+ # @returns [Integer] 0 if no injection detected, 1 if an injection was
63
+ # detected, 2 if there was an internal error, or 3 if SQL tokenization failed.
64
+ # @raise [Aikido::Zen::InternalsError] if there's a problem loading or
65
+ # calling libzen.
66
+ attach_function :detect_sql_injection_native, :detect_sql_injection,
67
+ [:pointer, :size_t, :pointer, :size_t, :int], :int
68
+ rescue LoadError, FFI::NotFoundError => err # rubocop:disable Lint/ShadowedException
69
+ # :nocov:
70
+
71
+ # Emit an $stderr warning at startup.
72
+ warn "Zen could not load its native extension #{libzen_name}: #{err}"
73
+
74
+ def self.detect_sql_injection(query, *)
75
+ attempt = format("%p for SQL injection", query)
76
+ raise InternalsError.new(attempt, "loading", libzen_name)
77
+ end
78
+
79
+ # :nocov:
80
+ else
81
+ # Analyzes the SQL query to detect if the provided user input is being
82
+ # passed as-is without escaping.
83
+ #
84
+ # @param query [String]
85
+ # @param input [String]
86
+ # @param dialect [Integer, #to_int] the SQL Dialect identifier in libzen.
87
+ # See {Aikido::Zen::Scanners::SQLInjectionScanner::DIALECTS}.
88
+ #
89
+ # @returns [Boolean]
90
+ # @raise [Aikido::Zen::InternalsError] if there's a problem loading or
91
+ # calling libzen.
92
+ def self.detect_sql_injection(query, input, dialect)
93
+ query_bytes = encode_safely(query)
94
+ input_bytes = encode_safely(input)
95
+
96
+ query_ptr = FFI::MemoryPointer.new(:uint8, query_bytes.bytesize)
97
+ input_ptr = FFI::MemoryPointer.new(:uint8, input_bytes.bytesize)
98
+
99
+ query_ptr.put_bytes(0, query_bytes)
100
+ input_ptr.put_bytes(0, input_bytes)
101
+
102
+ case detect_sql_injection_native(query_ptr, query_bytes.bytesize, input_ptr, input_bytes.bytesize, dialect)
103
+ when 0 then false
104
+ when 1 then true
105
+ when 2
106
+ attempt = format("%s query %p with input %p", dialect, query, input)
107
+ raise InternalsError.new(attempt, "calling detect_sql_injection in", libzen_name)
108
+ when 3
109
+ # SQL tokenization failed - return false (no injection detected)
110
+ false
111
+ end
112
+ end
113
+ end
114
+
115
+ class << self
116
+ private
117
+
118
+ def encode_safely(string)
119
+ string.encode("UTF-8", invalid: :replace, undef: :replace)
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikido::Zen
4
+ module Middleware
5
+ # Middleware that rejects requests from IPs blocked in the Aikido dashboard.
6
+ class AllowedAddressChecker
7
+ def initialize(app, config: Aikido::Zen.config, settings: Aikido::Zen.runtime_settings)
8
+ @app = app
9
+ @config = config
10
+ @settings = settings
11
+ end
12
+
13
+ def call(env)
14
+ request = Aikido::Zen::Middleware.request_from(env)
15
+
16
+ allowed_ips = @settings.endpoints[request.route].allowed_ips
17
+
18
+ if allowed_ips.empty? || allowed_ips.include?(request.ip)
19
+ @app.call(env)
20
+ else
21
+ @config.blocked_responder.call(request, :ip)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikido
4
+ module Zen
5
+ module Middleware
6
+ class AttackWaveProtector
7
+ def initialize(app, zen: Aikido::Zen, settings: Aikido::Zen.runtime_settings)
8
+ @app = app
9
+ @zen = zen
10
+ @settings = settings
11
+ end
12
+
13
+ def call(env)
14
+ response = @app.call(env)
15
+
16
+ context = @zen.current_context
17
+ protect(context)
18
+
19
+ response
20
+ end
21
+
22
+ # @api private
23
+ # Visible for testing.
24
+ def attack_wave?(context)
25
+ request = context.request
26
+ return false if request.nil?
27
+
28
+ # Bypass attack wave protection for allowed IPs
29
+ return false if @settings.allowed_ips.include?(request.ip)
30
+
31
+ @zen.attack_wave_detector.attack_wave?(context)
32
+ end
33
+
34
+ # @api private
35
+ # Visible for testing.
36
+ def protect(context)
37
+ if attack_wave?(context)
38
+ attack_wave = Aikido::Zen::Events::AttackWave.from_context(context)
39
+ @zen.track_attack_wave(attack_wave)
40
+ @zen.agent.report(attack_wave)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../context"
4
+
5
+ module Aikido::Zen
6
+ # @!visibility private
7
+ ENV_KEY = "aikido.context"
8
+
9
+ module Middleware
10
+ # Rack middleware that keeps the current context in a Thread/Fiber-local
11
+ # variable so that other parts of the agent/firewall can access it.
12
+ class ContextSetter
13
+ def initialize(app)
14
+ @app = app
15
+ end
16
+
17
+ def call(env)
18
+ Aikido::Zen.current_context = env[ENV_KEY] = Context.from_rack_env(env)
19
+
20
+ @app.call(env)
21
+ ensure
22
+ Aikido::Zen.current_context = nil
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikido
4
+ module Zen
5
+ module Middleware
6
+ # This middleware is responsible for detecting when a process has forked
7
+ # (e.g., in a Puma or Unicorn worker) and resetting the state of the
8
+ # Aikido Zen agent. It should be inserted early in the middleware stack.
9
+ class ForkDetector
10
+ def initialize(app)
11
+ @app = app
12
+ end
13
+
14
+ def call(env)
15
+ # This is the single, reliable trigger point for the fork check.
16
+ Aikido::Zen.check_and_handle_fork
17
+
18
+ @app.call(env)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ 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
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../context"
4
+
5
+ module Aikido::Zen
6
+ module Middleware
7
+ # Rack middleware that rejects requests from clients that are making too many
8
+ # requests to a given endpoint, based in the runtime configuration in the
9
+ # Aikido dashboard.
10
+ class RackThrottler
11
+ def initialize(
12
+ app,
13
+ config: Aikido::Zen.config,
14
+ settings: Aikido::Zen.runtime_settings,
15
+ detached_agent: Aikido::Zen.detached_agent
16
+ )
17
+ @app = app
18
+ @config = config
19
+ @settings = settings
20
+ @detached_agent = detached_agent
21
+ end
22
+
23
+ def call(env)
24
+ request = Aikido::Zen::Middleware.request_from(env)
25
+
26
+ if should_throttle?(request)
27
+ @config.rate_limited_responder.call(request)
28
+ else
29
+ @app.call(env)
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def should_throttle?(request)
36
+ # Bypass rate limiting for allowed IPs
37
+ return false if @settings.allowed_ips.include?(request.ip)
38
+
39
+ return false unless @settings.endpoints[request.route].rate_limiting.enabled?
40
+
41
+ result = @detached_agent.calculate_rate_limits(request)
42
+
43
+ return false unless result
44
+
45
+ request.env["aikido.rate_limiting"] = result
46
+ request.env["aikido.rate_limiting"].throttled?
47
+ end
48
+ end
49
+ end
50
+ end