aikido-zen 1.0.1.beta.2-arm64-linux-musl

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 (115) hide show
  1. checksums.yaml +7 -0
  2. data/.aikido +6 -0
  3. data/.ruby-version +1 -0
  4. data/.simplecov +26 -0
  5. data/.standard.yml +3 -0
  6. data/LICENSE +674 -0
  7. data/README.md +146 -0
  8. data/Rakefile +67 -0
  9. data/benchmarks/README.md +23 -0
  10. data/benchmarks/rails7.1_sql_injection.js +70 -0
  11. data/docs/banner.svg +202 -0
  12. data/docs/config.md +125 -0
  13. data/docs/rails.md +70 -0
  14. data/lib/aikido/zen/actor.rb +116 -0
  15. data/lib/aikido/zen/agent/heartbeats_manager.rb +66 -0
  16. data/lib/aikido/zen/agent.rb +179 -0
  17. data/lib/aikido/zen/api_client.rb +142 -0
  18. data/lib/aikido/zen/attack.rb +207 -0
  19. data/lib/aikido/zen/background_worker.rb +52 -0
  20. data/lib/aikido/zen/capped_collections.rb +68 -0
  21. data/lib/aikido/zen/collector/hosts.rb +15 -0
  22. data/lib/aikido/zen/collector/routes.rb +66 -0
  23. data/lib/aikido/zen/collector/sink_stats.rb +95 -0
  24. data/lib/aikido/zen/collector/stats.rb +111 -0
  25. data/lib/aikido/zen/collector/users.rb +30 -0
  26. data/lib/aikido/zen/collector.rb +144 -0
  27. data/lib/aikido/zen/config.rb +279 -0
  28. data/lib/aikido/zen/context/rack_request.rb +24 -0
  29. data/lib/aikido/zen/context/rails_request.rb +42 -0
  30. data/lib/aikido/zen/context.rb +112 -0
  31. data/lib/aikido/zen/detached_agent/agent.rb +78 -0
  32. data/lib/aikido/zen/detached_agent/front_object.rb +37 -0
  33. data/lib/aikido/zen/detached_agent/server.rb +41 -0
  34. data/lib/aikido/zen/detached_agent.rb +2 -0
  35. data/lib/aikido/zen/errors.rb +107 -0
  36. data/lib/aikido/zen/event.rb +71 -0
  37. data/lib/aikido/zen/internals.rb +102 -0
  38. data/lib/aikido/zen/libzen-v0.1.39-arm64-linux-musl.so +0 -0
  39. data/lib/aikido/zen/middleware/check_allowed_addresses.rb +26 -0
  40. data/lib/aikido/zen/middleware/middleware.rb +11 -0
  41. data/lib/aikido/zen/middleware/rack_throttler.rb +48 -0
  42. data/lib/aikido/zen/middleware/request_tracker.rb +192 -0
  43. data/lib/aikido/zen/middleware/set_context.rb +26 -0
  44. data/lib/aikido/zen/outbound_connection.rb +45 -0
  45. data/lib/aikido/zen/outbound_connection_monitor.rb +23 -0
  46. data/lib/aikido/zen/package.rb +22 -0
  47. data/lib/aikido/zen/payload.rb +50 -0
  48. data/lib/aikido/zen/rails_engine.rb +70 -0
  49. data/lib/aikido/zen/rate_limiter/breaker.rb +61 -0
  50. data/lib/aikido/zen/rate_limiter/bucket.rb +76 -0
  51. data/lib/aikido/zen/rate_limiter/result.rb +31 -0
  52. data/lib/aikido/zen/rate_limiter.rb +50 -0
  53. data/lib/aikido/zen/request/heuristic_router.rb +115 -0
  54. data/lib/aikido/zen/request/rails_router.rb +72 -0
  55. data/lib/aikido/zen/request/schema/auth_discovery.rb +86 -0
  56. data/lib/aikido/zen/request/schema/auth_schemas.rb +54 -0
  57. data/lib/aikido/zen/request/schema/builder.rb +121 -0
  58. data/lib/aikido/zen/request/schema/definition.rb +107 -0
  59. data/lib/aikido/zen/request/schema/empty_schema.rb +28 -0
  60. data/lib/aikido/zen/request/schema.rb +87 -0
  61. data/lib/aikido/zen/request.rb +103 -0
  62. data/lib/aikido/zen/route.rb +39 -0
  63. data/lib/aikido/zen/runtime_settings/endpoints.rb +49 -0
  64. data/lib/aikido/zen/runtime_settings/ip_set.rb +36 -0
  65. data/lib/aikido/zen/runtime_settings/protection_settings.rb +62 -0
  66. data/lib/aikido/zen/runtime_settings/rate_limit_settings.rb +47 -0
  67. data/lib/aikido/zen/runtime_settings.rb +65 -0
  68. data/lib/aikido/zen/scan.rb +75 -0
  69. data/lib/aikido/zen/scanners/path_traversal/helpers.rb +65 -0
  70. data/lib/aikido/zen/scanners/path_traversal_scanner.rb +63 -0
  71. data/lib/aikido/zen/scanners/shell_injection/helpers.rb +159 -0
  72. data/lib/aikido/zen/scanners/shell_injection_scanner.rb +64 -0
  73. data/lib/aikido/zen/scanners/sql_injection_scanner.rb +93 -0
  74. data/lib/aikido/zen/scanners/ssrf/dns_lookups.rb +27 -0
  75. data/lib/aikido/zen/scanners/ssrf/private_ip_checker.rb +97 -0
  76. data/lib/aikido/zen/scanners/ssrf_scanner.rb +265 -0
  77. data/lib/aikido/zen/scanners/stored_ssrf_scanner.rb +49 -0
  78. data/lib/aikido/zen/scanners.rb +7 -0
  79. data/lib/aikido/zen/sink.rb +118 -0
  80. data/lib/aikido/zen/sinks/action_controller.rb +83 -0
  81. data/lib/aikido/zen/sinks/async_http.rb +82 -0
  82. data/lib/aikido/zen/sinks/curb.rb +115 -0
  83. data/lib/aikido/zen/sinks/em_http.rb +85 -0
  84. data/lib/aikido/zen/sinks/excon.rb +121 -0
  85. data/lib/aikido/zen/sinks/file.rb +116 -0
  86. data/lib/aikido/zen/sinks/http.rb +95 -0
  87. data/lib/aikido/zen/sinks/httpclient.rb +97 -0
  88. data/lib/aikido/zen/sinks/httpx.rb +80 -0
  89. data/lib/aikido/zen/sinks/kernel.rb +34 -0
  90. data/lib/aikido/zen/sinks/mysql2.rb +33 -0
  91. data/lib/aikido/zen/sinks/net_http.rb +103 -0
  92. data/lib/aikido/zen/sinks/patron.rb +105 -0
  93. data/lib/aikido/zen/sinks/pg.rb +74 -0
  94. data/lib/aikido/zen/sinks/resolv.rb +62 -0
  95. data/lib/aikido/zen/sinks/socket.rb +80 -0
  96. data/lib/aikido/zen/sinks/sqlite3.rb +49 -0
  97. data/lib/aikido/zen/sinks/trilogy.rb +33 -0
  98. data/lib/aikido/zen/sinks/typhoeus.rb +78 -0
  99. data/lib/aikido/zen/sinks.rb +39 -0
  100. data/lib/aikido/zen/sinks_dsl.rb +226 -0
  101. data/lib/aikido/zen/synchronizable.rb +24 -0
  102. data/lib/aikido/zen/system_info.rb +84 -0
  103. data/lib/aikido/zen/version.rb +10 -0
  104. data/lib/aikido/zen/worker.rb +87 -0
  105. data/lib/aikido/zen.rb +206 -0
  106. data/lib/aikido-zen.rb +3 -0
  107. data/placeholder/.gitignore +4 -0
  108. data/placeholder/README.md +11 -0
  109. data/placeholder/Rakefile +75 -0
  110. data/placeholder/lib/placeholder.rb.template +3 -0
  111. data/placeholder/placeholder.gemspec.template +20 -0
  112. data/tasklib/bench.rake +94 -0
  113. data/tasklib/libzen.rake +132 -0
  114. data/tasklib/wrk.rb +88 -0
  115. metadata +204 -0
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikido::Zen
4
+ # An individual user input in a request, which may come from different
5
+ # sources (query string, body, cookies, etc).
6
+ class Payload
7
+ attr_reader :value, :source, :path
8
+
9
+ def initialize(value, source, path)
10
+ @value = value
11
+ @source = source
12
+ @path = path
13
+ end
14
+
15
+ UNKNOWN_PAYLOAD = Payload.new("unknown", "unknown", "unknown")
16
+
17
+ alias_method :to_s, :value
18
+
19
+ def ==(other)
20
+ other.is_a?(Payload) &&
21
+ other.value == value &&
22
+ other.source == source &&
23
+ other.path == path
24
+ end
25
+
26
+ def as_json
27
+ {
28
+ payload: value.to_s,
29
+ source: SOURCE_SERIALIZATIONS[source],
30
+ pathToPayload: path.to_s
31
+ }
32
+ end
33
+
34
+ SOURCE_SERIALIZATIONS = {
35
+ query: "query",
36
+ body: "body",
37
+ header: "headers",
38
+ cookie: "cookies",
39
+ route: "routeParams",
40
+ graphql: "graphql",
41
+ xml: "xml",
42
+ subdomain: "subdomains"
43
+ }
44
+
45
+ def inspect
46
+ val = (value.to_s.size > 128) ? value[0..125] + "..." : value
47
+ "#<Aikido::Zen::Payload #{source}(#{path}) #{val.inspect}>"
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_dispatch"
4
+
5
+ module Aikido::Zen
6
+ class RailsEngine < ::Rails::Engine
7
+ config.before_configuration do
8
+ # Access library configuration at `Rails.application.config.zen`.
9
+ config.zen = Aikido::Zen.config
10
+ end
11
+
12
+ initializer "aikido.add_middleware" do |app|
13
+ next unless config.zen.protect?
14
+
15
+ app.middleware.use Aikido::Zen::Middleware::SetContext
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
20
+
21
+ ActiveSupport.on_load(:action_controller) do
22
+ # Due to how Rails sets up its middleware chain, the routing is evaluated
23
+ # (and the Request object constructed) in the app that terminates the
24
+ # chain, so no amount of middleware will be able to access it.
25
+ #
26
+ # This way, we overwrite the Request object as early as we can in the
27
+ # request handling, so that by the time we start evaluating inputs, we
28
+ # have assigned the request correctly.
29
+ before_action { Aikido::Zen.current_context.update_request(request) }
30
+ end
31
+ end
32
+
33
+ initializer "aikido.configuration" do |app|
34
+ # Allow the logger to be configured before checking if disabled? so we can
35
+ # let the user know that the agent is disabled.
36
+ logger = ::Rails.logger
37
+ logger = ActiveSupport::TaggedLogging.new(logger) unless logger.respond_to?(:tagged)
38
+ app.config.zen.logger = logger.tagged("aikido")
39
+
40
+ app.config.zen.request_builder = Aikido::Zen::Context::RAILS_REQUEST_BUILDER
41
+
42
+ # Plug Rails' JSON encoder/decoder, but only if the user hasn't changed
43
+ # them for something else.
44
+ if app.config.zen.json_encoder == Aikido::Zen::Config::DEFAULT_JSON_ENCODER
45
+ app.config.zen.json_encoder = ActiveSupport::JSON.method(:encode)
46
+ end
47
+
48
+ if app.config.zen.json_decoder == Aikido::Zen::Config::DEFAULT_JSON_DECODER
49
+ app.config.zen.json_decoder = ActiveSupport::JSON.method(:decode)
50
+ end
51
+ end
52
+
53
+ config.after_initialize do
54
+ next unless config.zen.protect?
55
+
56
+ # Make sure this is run at the end of the initialization process, so
57
+ # that any gems required after aikido-zen are detected and patched
58
+ # accordingly.
59
+ Aikido::Zen.load_sinks!
60
+
61
+ # It's important we start after loading sinks, so we can report the installed packages
62
+ Aikido::Zen.start!
63
+
64
+ # Agent's bootstrap process has finished —Controllers are patched to block
65
+ # unwanted requests, sinks are loaded, scanners are running—, so we mark
66
+ # the agent as installed.
67
+ Aikido::Zen.middleware_installed!
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "bucket"
4
+
5
+ module Aikido::Zen
6
+ # @api private
7
+ #
8
+ # Circuit breaker that rate limits internal API requests in two ways: By using
9
+ # a sliding window, to allow only a certain number of events over that window,
10
+ # and with the ability of manually being tripped open when the API responds to
11
+ # a request with a 429.
12
+ class RateLimiter::Breaker
13
+ def initialize(config: Aikido::Zen.config, clock: RateLimiter::Bucket::DEFAULT_CLOCK)
14
+ @config = config
15
+ @clock = clock
16
+
17
+ @bucket = RateLimiter::Bucket.new(
18
+ ttl: config.client_rate_limit_period,
19
+ max_size: config.client_rate_limit_max_events,
20
+ clock: clock
21
+ )
22
+ @opened_at = nil
23
+ end
24
+
25
+ # Trip the circuit open to force all events to be throttled until the
26
+ # deadline passes.
27
+ #
28
+ # @see Aikido::Zen::Config#server_rate_limit_deadline
29
+ # @return [void]
30
+ def open!
31
+ @opened_at = @clock.call
32
+ end
33
+
34
+ # @param event_type [String] an event type which we'll use to decide
35
+ # if we should throttle it.
36
+ # @return [Boolean]
37
+ def throttle?(event_type)
38
+ return true if open? && !try_close
39
+
40
+ result = @bucket.increment(event_type)
41
+ result.throttled?
42
+ end
43
+
44
+ # @!visibility private
45
+ # @return [Boolean]
46
+ def open?
47
+ @opened_at
48
+ end
49
+
50
+ private
51
+
52
+ def past_deadline?
53
+ @opened_at < @clock.call - @config.server_rate_limit_deadline
54
+ end
55
+
56
+ def try_close
57
+ @opened_at = nil if past_deadline?
58
+ @opened_at.nil?
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../synchronizable"
4
+ require_relative "result"
5
+
6
+ module Aikido::Zen
7
+ # This models a "sliding window" rate limiting bucket (where we keep a bucket
8
+ # per endpoint). The timestamps of requests are kept grouped by client, and
9
+ # when a new request is made, we check if the number of requests falls within
10
+ # the configured limit.
11
+ #
12
+ # @example
13
+ # bucket = Aikido::Zen::RateLimiter::Bucket.new(ttl: 60, max_size: 3)
14
+ # bucket.increment("1.2.3.4") #=> true (count for this key: 1)
15
+ # bucket.increment("1.2.3.4") #=> true (count for this key: 2)
16
+ #
17
+ # # 30 seconds go by
18
+ # bucket.increment("1.2.3.4") #=> true (count for this key: 3)
19
+ #
20
+ # # 20 more seconds go by
21
+ # bucket.increment("1.2.3.4") #=> false (count for this key: 3)
22
+ #
23
+ # # 20 more seconds go by
24
+ # bucket.increment("1.2.3.4") #=> true (count for this key: 2)
25
+ #
26
+ class RateLimiter::Bucket
27
+ prepend Synchronizable
28
+
29
+ # @!visibility private
30
+ #
31
+ # Use the monotonic clock to ensure time differences are consistent
32
+ # and not affected by timezones.or daylight savings changes.
33
+ DEFAULT_CLOCK = -> { Process.clock_gettime(Process::CLOCK_MONOTONIC).round }
34
+
35
+ def initialize(ttl:, max_size:, clock: DEFAULT_CLOCK)
36
+ @ttl = ttl
37
+ @max_size = max_size
38
+ @data = Hash.new { |h, k| h[k] = [] }
39
+ @clock = clock
40
+ end
41
+
42
+ # Increments the key if the number of entries within the current TTL window
43
+ # is below the configured threshold.
44
+ #
45
+ # @param key [String] discriminating key to identify a client.
46
+ # See {Aikido::Zen::Config#rate_limiting_discriminator}.
47
+ #
48
+ # @return [Aikido::Zen::RateLimiter::Result] the result of the operation and
49
+ # statistics on this bucket for the given key.
50
+ def increment(key)
51
+ synchronize do
52
+ time = @clock.call
53
+ evict(key, at: time)
54
+
55
+ entries = @data[key]
56
+ throttled = entries.size >= @max_size
57
+
58
+ entries << time unless throttled
59
+
60
+ RateLimiter::Result.new(
61
+ throttled: throttled,
62
+ discriminator: key,
63
+ current_requests: entries.size,
64
+ max_requests: @max_size,
65
+ time_remaining: @ttl - (time - entries.min)
66
+ )
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ def evict(key, at: @clock.call)
73
+ synchronize { @data[key].delete_if { |time| time < (at - @ttl) } }
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,31 @@
1
+ module Aikido::Zen
2
+ # Holds the stats after checking if a request should be rate limited, which
3
+ # will be added to the Rack env.
4
+ class RateLimiter::Result
5
+ # @return [String] the output of the configured discriminator block, used to
6
+ # uniquely identify a client (e.g. the remote IP).
7
+ attr_reader :discriminator
8
+
9
+ # @return [Integer] number of requests for the client in the current window.
10
+ attr_reader :current_requests
11
+
12
+ # @return [Integer] configured max number of requests per client.
13
+ attr_reader :max_requests
14
+
15
+ # @return [Integer] number of seconds remaining until the window resets.
16
+ attr_reader :time_remaining
17
+
18
+ def initialize(throttled:, discriminator:, current_requests:, max_requests:, time_remaining:)
19
+ @throttled = throttled
20
+ @discriminator = discriminator
21
+ @current_requests = current_requests
22
+ @max_requests = max_requests
23
+ @time_remaining = time_remaining
24
+ end
25
+
26
+ # @return [Boolean] whether the current request was throttled or not.
27
+ def throttled?
28
+ @throttled
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "synchronizable"
4
+ require_relative "middleware/rack_throttler"
5
+
6
+ module Aikido::Zen
7
+ # Keeps track of all requests in this process, broken up by Route and further
8
+ # discriminated by client. Provides a single method that checks if a certain
9
+ # Request needs to be throttled or not.
10
+ class RateLimiter
11
+ prepend Synchronizable
12
+
13
+ def initialize(
14
+ config: Aikido::Zen.config,
15
+ settings: Aikido::Zen.runtime_settings
16
+ )
17
+ @config = config
18
+ @settings = settings
19
+ @buckets = Hash.new { |store, route|
20
+ synchronize {
21
+ settings = settings_for(route)
22
+ store[route] = Bucket.new(ttl: settings.period, max_size: settings.max_requests)
23
+ }
24
+ }
25
+ end
26
+
27
+ # Calculate based on the configuration whether a request will be
28
+ # rate-limited or not.
29
+ #
30
+ # @param request [Aikido::Zen::Request]
31
+ # @return [Aikido::Zen::RateLimiter::Result, nil]
32
+ def calculate_rate_limits(request)
33
+ settings = settings_for(request.route)
34
+ return nil unless settings.enabled?
35
+
36
+ bucket = @buckets[request.route]
37
+ key = @config.rate_limiting_discriminator.call(request)
38
+ bucket.increment(key)
39
+ end
40
+
41
+ private
42
+
43
+ def settings_for(route)
44
+ @settings.endpoints[route].rate_limiting
45
+ end
46
+ end
47
+ end
48
+
49
+ require_relative "rate_limiter/bucket"
50
+ require_relative "rate_limiter/breaker"
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ipaddr"
4
+ require_relative "../route"
5
+ require_relative "../request"
6
+
7
+ module Aikido::Zen
8
+ # Simple router implementation that just identifies the currently requested
9
+ # URL as a route, attempting to heuristically substitute any path segments
10
+ # that may look like a parameterized value by something descriptive.
11
+ #
12
+ # For example, "/categories/123/events/2024-10-01" would be matched as
13
+ # "/categories/:number/events/:date"
14
+ class Request::HeuristicRouter
15
+ # @param request [Aikido::Zen::Request]
16
+ # @return [Aikido::Zen::Route, nil]
17
+ def recognize(request)
18
+ path = parameterize(request.path)
19
+ Route.new(verb: request.request_method, path: path)
20
+ end
21
+
22
+ private def parameterize(path)
23
+ return if path.nil?
24
+
25
+ path = path.split("/").map { |part| parameterize_segment(part) }.join("/")
26
+ path.prepend("/") unless path.start_with?("/")
27
+ path.chomp!("/") if path.size > 1
28
+ path
29
+ end
30
+
31
+ private def parameterize_segment(segment)
32
+ case segment
33
+ when ULID
34
+ ":ulid"
35
+ when OBJECT_ID
36
+ ":objectId"
37
+ when NUMBER
38
+ ":number"
39
+ when UUID
40
+ ":uuid"
41
+ when DATE
42
+ ":date"
43
+ when EMAIL
44
+ ":email"
45
+ when IP
46
+ ":ip"
47
+ when HASH
48
+ ":hash"
49
+ when SecretMatcher
50
+ ":secret"
51
+ else
52
+ segment
53
+ end
54
+ end
55
+
56
+ NUMBER = /\A\d+\z/
57
+ HEX = /\A[a-f0-9]+\z/i
58
+ DATE = /\A\d{4}-\d{2}-\d{2}|\d{2}-\d{2}-\d{4}\z/
59
+ UUID = /\A
60
+ (?:[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}
61
+ | 00000000-0000-0000-0000-000000000000
62
+ | ffffffff-ffff-ffff-ffff-ffffffffffff
63
+ )\z/ix
64
+ ULID = /\A[0-9A-HJKMNP-TV-Z]{26}\z/i
65
+ OBJECT_ID = /\A[0-9a-f]{24}\z/i
66
+ EMAIL = /\A
67
+ [a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+
68
+ @
69
+ [a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?
70
+ (?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*
71
+ \z/x
72
+ IP = ->(segment) {
73
+ IPAddr::RE_IPV4ADDRLIKE.match?(segment) ||
74
+ IPAddr::RE_IPV6ADDRLIKE_COMPRESSED.match?(segment) ||
75
+ IPAddr::RE_IPV6ADDRLIKE_FULL.match?(segment)
76
+ }
77
+ HASH = ->(segment) { [32, 40, 64, 128].include?(segment.size) && HEX === segment }
78
+
79
+ class SecretMatcher
80
+ # Decides if a given string looks random enough to be a "secret".
81
+ #
82
+ # @param candidate [String]
83
+ # @return [Boolean]
84
+ def self.===(candidate)
85
+ new(candidate).matches?
86
+ end
87
+
88
+ private def initialize(string)
89
+ @string = string
90
+ end
91
+
92
+ def matches?
93
+ return false if @string.size <= MIN_LENGTH
94
+ return false if SEPARATORS === @string
95
+ return false unless DIGIT === @string
96
+ return false if [LOWER, UPPER, SPECIAL].none? { |pattern| pattern === @string }
97
+
98
+ ratios = @string.chars.each_cons(MIN_LENGTH).map do |window|
99
+ window.to_set.size / MIN_LENGTH.to_f
100
+ end
101
+
102
+ ratios.sum / ratios.size > SECRET_THRESHOLD
103
+ end
104
+
105
+ MIN_LENGTH = 10
106
+ SECRET_THRESHOLD = 0.75
107
+
108
+ LOWER = /[[:lower:]]/
109
+ UPPER = /[[:upper:]]/
110
+ DIGIT = /[[:digit:]]/
111
+ SPECIAL = /[!#\$%^&*|;:<>]/
112
+ SEPARATORS = /[[:space:]]|-/
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../route"
4
+ require_relative "../request"
5
+
6
+ module Aikido::Zen
7
+ # The Rails router relies on introspecting the routes defined in the Rails
8
+ # app to match the current request to the correct route, building Route
9
+ # objects that have the exact pattern defined by the developer, rather than
10
+ # a heuristic approximation.
11
+ #
12
+ # For example, given the following route definitions:
13
+ #
14
+ # resources :posts do
15
+ # resources :comments
16
+ # end
17
+ #
18
+ # The router will map a request to "/posts/123/comments/234" to
19
+ # "/posts/:post_id/comments/:id(.:format)".
20
+ #
21
+ # @see Aikido::Zen::Router::HeuristicRouter
22
+ class Request::RailsRouter
23
+ def initialize(route_set)
24
+ @route_set = route_set
25
+ end
26
+
27
+ def recognize(request)
28
+ recognize_in_route_set(request, @route_set)
29
+ end
30
+
31
+ private def recognize_in_route_set(request, route_set, prefix: nil)
32
+ route_set.router.recognize(request) do |route, _|
33
+ app = route.app
34
+ next unless app.matches?(request)
35
+
36
+ if app.dispatcher?
37
+ return build_route(route, request, prefix: prefix)
38
+ end
39
+
40
+ if app.engine?
41
+ # If the SCRIPT_NAME has any path parameters, we want those to be
42
+ # captured by the router. (eg `mount API => "/api/:version/`)
43
+ prefix = ActionDispatch::Routing::RouteWrapper.new(route).path
44
+ return recognize_in_route_set(request, app.rack_app.routes, prefix: prefix)
45
+ end
46
+
47
+ if app.rack_app.respond_to?(:redirect?) && app.rack_app.redirect?
48
+ return build_route(route, request, prefix: prefix)
49
+ end
50
+
51
+ # At this point we're matching plain Rack apps, where Rails does not
52
+ # remove the SCRIPT_NAME from PATH_INFO, so we should avoid adding
53
+ # SCRIPT_NAME twice.
54
+ return build_route(route, request, prefix: nil)
55
+ end
56
+
57
+ nil
58
+ end
59
+
60
+ private def build_route(route, request, prefix: request.script_name)
61
+ route_wrapper = ActionDispatch::Routing::RouteWrapper.new(route)
62
+
63
+ path = if prefix.present?
64
+ File.join(prefix.to_s, route_wrapper.path).chomp("/")
65
+ else
66
+ route_wrapper.path
67
+ end
68
+
69
+ Aikido::Zen::Route.new(verb: request.request_method, path: path)
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "auth_schemas"
4
+
5
+ module Aikido::Zen
6
+ class Request::Schema
7
+ class AuthDiscovery
8
+ def initialize(context)
9
+ @context = context
10
+ end
11
+
12
+ def schemas
13
+ schemas = []
14
+ schemas << extract_from_authorization_header if headers["Authorization"]
15
+ schemas.concat(extract_from_headers)
16
+ schemas.concat(extract_from_cookies)
17
+
18
+ AuthSchemas.new(schemas)
19
+ end
20
+
21
+ private
22
+
23
+ def extract_from_authorization_header
24
+ type, _ = headers["Authorization"].to_s.split(/\s+/, 2)
25
+
26
+ if AUTHORIZATION_SCHEMES.include?(type.to_s.downcase)
27
+ AuthSchemas::Authorization.new(type)
28
+ else
29
+ AuthSchemas::ApiKey.new(:header, "Authorization")
30
+ end
31
+ end
32
+
33
+ def extract_from_headers
34
+ (headers.keys & COMMON_API_KEY_HEADERS)
35
+ .map { |header| AuthSchemas::ApiKey.new(:header, header) }
36
+ end
37
+
38
+ def extract_from_cookies
39
+ cookie_names = @context.payload_sources[:cookie].keys.map(&:downcase)
40
+
41
+ (cookie_names & COMMON_COOKIE_NAMES)
42
+ .map { |cookie| AuthSchemas::ApiKey.new(:cookie, cookie) }
43
+ end
44
+
45
+ def headers
46
+ @context.request.normalized_headers
47
+ end
48
+
49
+ AUTHORIZATION_SCHEMES = %w[
50
+ basic
51
+ bearer
52
+ digest
53
+ dpop
54
+ gnap
55
+ hoba
56
+ mutal
57
+ negotiate
58
+ privatetoken
59
+ scram-sha-1
60
+ scram-sha-256
61
+ vapid
62
+ ].freeze
63
+
64
+ COMMON_API_KEY_HEADERS = %w[
65
+ Apikey
66
+ Api-Key
67
+ Token
68
+ X-Api-Key
69
+ X-Token
70
+ ]
71
+
72
+ COMMON_COOKIE_NAMES = %w[
73
+ user_id
74
+ auth
75
+ session
76
+ jwt
77
+ token
78
+ sid
79
+ connect.sid
80
+ auth_token
81
+ access_token
82
+ refresh_token
83
+ ]
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikido::Zen
4
+ class Request::Schema
5
+ class AuthSchemas
6
+ attr_reader :schemas
7
+
8
+ def initialize(schemas)
9
+ @schemas = schemas
10
+ end
11
+
12
+ def merge(other)
13
+ self.class.new((schemas + other.schemas).uniq)
14
+ end
15
+ alias_method :|, :merge
16
+
17
+ def as_json
18
+ @schemas.map(&:as_json) unless @schemas.empty?
19
+ end
20
+
21
+ def self.from_json(schemas_array)
22
+ return NONE if !schemas_array || schemas_array.empty?
23
+
24
+ AuthSchemas.new(schemas_array.map do |schema|
25
+ if schema[:type] == "http"
26
+ Authorization.new(schema[:scheme])
27
+ elsif schema[:type] == "apiKey"
28
+ ApiKey.new(schema[:in], schema[:name])
29
+ else
30
+ raise "Invalid schema type: #{schema[:type]}"
31
+ end
32
+ end)
33
+ end
34
+
35
+ def ==(other)
36
+ other.is_a?(self.class) && schemas == other.schemas
37
+ end
38
+
39
+ NONE = new([])
40
+
41
+ Authorization = Struct.new(:scheme) do
42
+ def as_json
43
+ {type: "http", scheme: scheme.downcase}
44
+ end
45
+ end
46
+
47
+ ApiKey = Struct.new(:location, :name) do
48
+ def as_json
49
+ {type: "apiKey", in: location, name: name}.compact
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end