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,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "delegate"
4
+
5
+ module Aikido::Zen
6
+ # Wrapper around Rack::Request-like objects to add some behavior.
7
+ class Request < SimpleDelegator
8
+ # @return [String] identifier of the framework handling this HTTP request.
9
+ attr_reader :framework
10
+
11
+ # @return [Aikido::Zen::Router]
12
+ attr_reader :router
13
+
14
+ # The current user, if set by the host app.
15
+ #
16
+ # @return [Aikido::Zen::Actor, nil]
17
+ # @see Aikido::Zen.track_user
18
+ attr_accessor :actor
19
+
20
+ def initialize(delegate, config = Aikido::Zen.config, framework:, router:)
21
+ super(delegate)
22
+ @config = config
23
+ @framework = framework
24
+ @router = router
25
+ end
26
+
27
+ def __setobj__(delegate) # :nodoc:
28
+ super
29
+ @route = @normalized_header = nil
30
+ end
31
+
32
+ # @return [Aikido::Zen::Route] the framework route being requested.
33
+ def route
34
+ @route ||= @router.recognize(self)
35
+ end
36
+
37
+ # @return [Aikido::Zen::Request::Schema, nil]
38
+ def schema
39
+ @schema ||= Aikido::Zen::Request::Schema.build
40
+ end
41
+
42
+ # @return [String] the IP address of the client making the request.
43
+ def client_ip
44
+ return @client_ip if @client_ip
45
+
46
+ if @config.client_ip_header
47
+ value = env[@config.client_ip_header]
48
+ if Resolv::AddressRegex.match?(value)
49
+ @client_ip = value
50
+ else
51
+ @config.logger.warn("Invalid IP address in custom client IP header `#{@config.client_ip_header}`: `#{value}`")
52
+ end
53
+ end
54
+
55
+ @client_ip ||= respond_to?(:remote_ip) ? remote_ip : ip
56
+ end
57
+
58
+ # Map the CGI-style env Hash into "pretty-looking" headers, preserving the
59
+ # values as-is. For example, HTTP_ACCEPT turns into "Accept", CONTENT_TYPE
60
+ # turns into "Content-Type", and HTTP_X_FORWARDED_FOR turns into
61
+ # "X-Forwarded-For".
62
+ #
63
+ # @return [Hash<String, String>]
64
+ def normalized_headers
65
+ @normalized_headers ||= env.slice(*BLESSED_CGI_HEADERS)
66
+ .merge(env.select { |key, _| key.start_with?("HTTP_") })
67
+ .transform_keys { |header|
68
+ name = header.sub(/^HTTP_/, "").downcase
69
+ name.split("_").map { |part| part[0].upcase + part[1..] }.join("-")
70
+ }
71
+ end
72
+
73
+ def as_json
74
+ {
75
+ method: request_method.upcase,
76
+ url: url,
77
+ ipAddress: client_ip,
78
+ userAgent: user_agent,
79
+ source: framework,
80
+ route: route&.path
81
+ }
82
+ end
83
+
84
+ BLESSED_CGI_HEADERS = %w[CONTENT_TYPE CONTENT_LENGTH]
85
+ end
86
+ end
87
+
88
+ require_relative "request/schema"
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikido::Zen
4
+ # Routes keep information about the mapping defined in the current web
5
+ # framework to go from a given HTTP request to the code that handles said
6
+ # request.
7
+ class Route
8
+ def self.from_json(data)
9
+ new(
10
+ verb: data[:method],
11
+ path: data[:path]
12
+ )
13
+ end
14
+
15
+ # @return [String] the HTTP verb used to request this route.
16
+ attr_reader :verb
17
+
18
+ # @return [String] the URL pattern used to match request paths. For
19
+ # example "/users/:id".
20
+ attr_reader :path
21
+
22
+ def initialize(verb:, path:)
23
+ @verb = verb
24
+ @path = path
25
+ end
26
+
27
+ def as_json
28
+ {method: verb, path: path}
29
+ end
30
+
31
+ def ==(other)
32
+ other.is_a?(Route) &&
33
+ other.verb == verb &&
34
+ other.path == path
35
+ end
36
+ alias_method :eql?, :==
37
+
38
+ def hash
39
+ [verb, path].hash
40
+ end
41
+
42
+ # Sort routes by wildcard matching order deterministically:
43
+ #
44
+ # 1. Exact path before wildcard path
45
+ # 2. Fewer wildcards in path relative to path length
46
+ # 3. Earliest wildcard position in path
47
+ # 4. Exact verb before wildcard verb
48
+ # 5. Lexicographic path (tie-break)
49
+ # 6. Lexicographic verb (tie-break)
50
+ #
51
+ # @return [Array] the sort key
52
+ def sort_key
53
+ @sort_key ||= begin
54
+ stars = []
55
+ i = -1
56
+ while (i = path.index("*", i + 1))
57
+ stars << i
58
+ end
59
+
60
+ [
61
+ stars.empty? ? 0 : 1,
62
+ stars.length - path.length,
63
+ stars,
64
+ (verb == "*") ? 1 : 0,
65
+ path,
66
+ verb
67
+ ].freeze
68
+ end
69
+ end
70
+
71
+ def match?(other)
72
+ other.is_a?(Route) &&
73
+ pattern(verb).match?(other.verb) &&
74
+ pattern(path).match?(other.path)
75
+ end
76
+
77
+ def inspect
78
+ "#<#{self.class.name} #{verb} #{path.inspect}>"
79
+ end
80
+
81
+ # Construct a regular expression equivalent to the wildcard string,
82
+ # where '*' is the wildcard operator.
83
+ #
84
+ # The resulting pattern matches the entire input, allows an optional
85
+ # trailing slash, and is case-insensitive.
86
+ #
87
+ # All other special characters in the regular expression are escaped
88
+ # so that they are treated literally.
89
+ #
90
+ # @param string [String] wildcard string
91
+ # @return [Regexp] regular expression matching the wildcard string
92
+ private def pattern(string)
93
+ /^#{Regexp.escape(string).gsub("\\*", ".*")}\/?$/i
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../route"
4
+ require_relative "protection_settings"
5
+
6
+ module Aikido::Zen
7
+ # Wraps the list of endpoint protection settings, providing an interface for
8
+ # checking the settings for any given route. If the route has no configured
9
+ # settings, that will return the singleton
10
+ # {RuntimeSettings::ProtectionSettings.none}.
11
+ #
12
+ # @example
13
+ # endpoint = runtime_settings.endpoints[request.route]
14
+ # block_request unless endpoint.allows?(request.ip)
15
+ class RuntimeSettings::Endpoints
16
+ # @param data [Array<Hash>]
17
+ # @return [Aikido::Zen::RuntimeSettings::Endpoints]
18
+ def self.from_json(data)
19
+ endpoint_pairs = Array(data).map do |value|
20
+ route = Route.new(verb: value["method"], path: value["route"])
21
+ settings = RuntimeSettings::ProtectionSettings.from_json(value)
22
+ [route, settings]
23
+ end
24
+
25
+ # Sort endpoints by wildcard matching order
26
+ endpoint_pairs.sort_by! do |route, settings|
27
+ route.sort_key
28
+ end
29
+
30
+ new(endpoint_pairs.to_h)
31
+ end
32
+
33
+ # @param endpoints [Hash] the endpoints in wildcard matching order
34
+ # @return [Aikido::Zen::RuntimeSettings::Endpoints]
35
+ def initialize(endpoints = {})
36
+ @endpoints = endpoints
37
+ @endpoints.default = RuntimeSettings::ProtectionSettings.none
38
+ end
39
+
40
+ # @param route [Aikido::Zen::Route]
41
+ # @return [Aikido::Zen::RuntimeSettings::ProtectionSettings]
42
+ def [](route)
43
+ return @endpoints[route] if @endpoints.key?(route)
44
+
45
+ # Wildcard endpoint matching
46
+
47
+ @endpoints.each do |pattern, settings|
48
+ return settings if pattern.match?(route)
49
+ end
50
+
51
+ @endpoints.default
52
+ end
53
+
54
+ # @param route [Aikido::Zen::Route]
55
+ # @return [Array<Aikido::Zen::RuntimeSettings::ProtectionSettings>]
56
+ def match(route)
57
+ matches = []
58
+
59
+ @endpoints.each do |pattern, settings|
60
+ matches << settings if pattern.match?(route)
61
+ end
62
+
63
+ matches << @endpoints.default if matches.empty?
64
+
65
+ matches
66
+ end
67
+
68
+ # @!visibility private
69
+ def ==(other)
70
+ other.is_a?(RuntimeSettings::Endpoints) && to_h == other.to_h
71
+ end
72
+
73
+ # @!visibility private
74
+ protected def to_h
75
+ @endpoints
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ipaddr"
4
+
5
+ module Aikido::Zen
6
+ # Models a list of IP addresses or CIDR blocks, where we can check if a given
7
+ # address is part of any of the members.
8
+ class RuntimeSettings::IPSet
9
+ def self.from_json(ips)
10
+ new(Array(ips).map { |ip| IPAddr.new(ip) })
11
+ end
12
+
13
+ def initialize(ips = Set.new)
14
+ @ips = ips.to_set
15
+ end
16
+
17
+ def empty?
18
+ @ips.empty?
19
+ end
20
+
21
+ def include?(ip)
22
+ @ips.any? { |pattern| pattern === ip }
23
+ end
24
+ alias_method :===, :include?
25
+
26
+ def ==(other)
27
+ other.is_a?(RuntimeSettings::IPSet) && to_set == other.to_set
28
+ end
29
+
30
+ protected
31
+
32
+ def to_set
33
+ @ips
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "ip_set"
4
+ require_relative "rate_limit_settings"
5
+
6
+ module Aikido::Zen
7
+ # Models the settings for a given Route as configured in the Aikido UI.
8
+ class RuntimeSettings::ProtectionSettings
9
+ # @return [Aikido::Zen::RuntimeSettings::ProtectionSettings] singleton
10
+ # instance for endpoints with no configured protections on a given route,
11
+ # that can be used as a default value for routes.
12
+ def self.none
13
+ @no_settings ||= new
14
+ end
15
+
16
+ # Initialize settings from an API response.
17
+ #
18
+ # @param data [Hash] the deserialized JSON data.
19
+ # @option data [Boolean] "forceProtectionOff" whether the user has
20
+ # disabled attack protection for this route.
21
+ # @option data [Array<String>] "allowedIPAddresses" the list of IPs that
22
+ # can make requests to this endpoint.
23
+ # @option data [Hash] "rateLimiting" the rate limiting options for this
24
+ # endpoint. See {Aikido::Zen::RuntimeSettings::RateLimitSettings.from_json}.
25
+ #
26
+ # @return [Aikido::Zen::RuntimeSettings::ProtectionSettings]
27
+ # @raise [IPAddr::InvalidAddressError] if any of the IPs in
28
+ # "allowedIPAddresses" is not a valid address or family.
29
+ def self.from_json(data)
30
+ ips = RuntimeSettings::IPSet.from_json(data["allowedIPAddresses"])
31
+ rate_limiting = RuntimeSettings::RateLimitSettings.from_json(data["rateLimiting"])
32
+
33
+ new(
34
+ protected: !data["forceProtectionOff"],
35
+ allowed_ips: ips,
36
+ rate_limiting: rate_limiting
37
+ )
38
+ end
39
+
40
+ # @return [Aikido::Zen::RuntimeSettings::IPSet] list of IP addresses which
41
+ # are allowed to make requests on this route. If empty, all IP addresses
42
+ # are allowed.
43
+ attr_reader :allowed_ips
44
+
45
+ # @return [Aikido::Zen::RuntimeSettings::RateLimitSettings]
46
+ attr_reader :rate_limiting
47
+
48
+ def initialize(
49
+ protected: true,
50
+ allowed_ips: RuntimeSettings::IPSet.new,
51
+ rate_limiting: RuntimeSettings::RateLimitSettings.disabled
52
+ )
53
+ @protected = !!protected
54
+ @rate_limiting = rate_limiting
55
+ @allowed_ips = allowed_ips
56
+ end
57
+
58
+ def protected?
59
+ @protected
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikido::Zen
4
+ # Simple data object that holds the configuration for rate limiting a given
5
+ # endpoint.
6
+ class RuntimeSettings::RateLimitSettings
7
+ # Initialize the settings from an API response.
8
+ #
9
+ # @param data [Hash] the deserialized JSON data.
10
+ # @option data [Boolean] "enabled"
11
+ # @option data [Integer] "maxRequests"
12
+ # @option data [Integer] "windowSizeInMS"
13
+ #
14
+ # @return [Aikido::Zen::RateLimitSettings]
15
+ def self.from_json(data)
16
+ new(
17
+ enabled: !!data["enabled"],
18
+ max_requests: Integer(data["maxRequests"]),
19
+ period: Integer(data["windowSizeInMS"]) / 1000
20
+ )
21
+ end
22
+
23
+ # Initializes a disabled object that we can use as a default value for
24
+ # endpoints that have not configured rate limiting.
25
+ #
26
+ # @return [Aikido::Zen::RuntimeSettings::RateLimitSettings]
27
+ def self.disabled
28
+ new(enabled: false)
29
+ end
30
+
31
+ # @return [Integer] the fixed window to bucket requests in, in seconds.
32
+ attr_reader :period
33
+
34
+ # @return [Integer]
35
+ attr_reader :max_requests
36
+
37
+ def initialize(enabled: false, max_requests: 1000, period: 60)
38
+ @enabled = enabled
39
+ @period = period
40
+ @max_requests = max_requests
41
+ end
42
+
43
+ def enabled?
44
+ @enabled
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikido::Zen
4
+ # Stores the firewall configuration sourced from the Aikido dashboard. This
5
+ # object is updated by the Agent regularly.
6
+ #
7
+ # Because the RuntimeSettings object can be modified in runtime, it implements
8
+ # the {Observable} API, allowing you to subscribe to updates. These are
9
+ # triggered whenever #update_from_json makes a change (i.e. if the settings
10
+ # don't change, no update is triggered).
11
+ #
12
+ # You can subscribe to changes with +#add_observer(object, func_name)+, which
13
+ # will call the function passing the settings as an argument.
14
+ RuntimeSettings = Struct.new(:updated_at, :heartbeat_interval, :endpoints, :blocked_user_ids, :allowed_ips, :received_any_stats, :blocking_mode) do
15
+ def initialize(*)
16
+ super
17
+ self.endpoints ||= RuntimeSettings::Endpoints.new
18
+ self.allowed_ips ||= RuntimeSettings::IPSet.new
19
+ end
20
+
21
+ # @!attribute [rw] updated_at
22
+ # @return [Time] when these settings were updated in the Aikido dashboard.
23
+
24
+ # @!attribute [rw] heartbeat_interval
25
+ # @return [Integer] duration in seconds between heartbeat requests to the
26
+ # Aikido server.
27
+
28
+ # @!attribute [rw] received_any_stats
29
+ # @return [Boolean] whether the Aikido server has received any data from
30
+ # this application.
31
+
32
+ # @!attribute [rw] endpoints
33
+ # @return [Aikido::Zen::RuntimeSettings::Endpoints]
34
+
35
+ # @!attribute [rw] blocked_user_ids
36
+ # @return [Array]
37
+
38
+ # @!attribute [rw] allowed_ips
39
+ # @return [Aikido::Zen::RuntimeSettings::IPSet]
40
+
41
+ # Parse and interpret the JSON response from the core API with updated
42
+ # settings, and apply the changes. This will also notify any subscriber
43
+ # to updates
44
+ #
45
+ # @param data [Hash] the decoded JSON payload from the /api/runtime/config
46
+ # API endpoint.
47
+ #
48
+ # @return [bool]
49
+ def update_from_json(data)
50
+ last_updated_at = updated_at
51
+
52
+ self.updated_at = Time.at(data["configUpdatedAt"].to_i / 1000)
53
+ self.heartbeat_interval = data["heartbeatIntervalInMS"].to_i / 1000
54
+ self.endpoints = RuntimeSettings::Endpoints.from_json(data["endpoints"])
55
+ self.blocked_user_ids = data["blockedUserIds"]
56
+ self.allowed_ips = RuntimeSettings::IPSet.from_json(data["allowedIPAddresses"])
57
+ self.received_any_stats = data["receivedAnyStats"]
58
+ self.blocking_mode = data["block"]
59
+
60
+ updated_at != last_updated_at
61
+ end
62
+ end
63
+ end
64
+
65
+ require_relative "runtime_settings/ip_set"
66
+ require_relative "runtime_settings/endpoints"
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikido::Zen
4
+ # Scans track information about a single call made by one of our Sinks
5
+ # including whether it was detected as an attack or how long it took.
6
+ class Scan
7
+ # @return [Aikido::Zen::Sink] the originating Sink.
8
+ attr_reader :sink
9
+
10
+ # @return [Aikido::Zen::Context] the current Context, wrapping the HTTP
11
+ # request during which this scan was performed.
12
+ attr_reader :context
13
+
14
+ # @return [Aikido::Zen::Attack, nil] a detected Attack, or
15
+ # +nil+ if the scan was considered safe.
16
+ attr_reader :attack
17
+
18
+ # @return [Float, nil] duration in (fractional) seconds of the scan.
19
+ attr_reader :duration
20
+
21
+ # @return [Array<Hash>] list of captured exceptions while scanning.
22
+ attr_reader :errors
23
+
24
+ # @param sink [Aikido::Zen::Sink]
25
+ # @param context [Aikido::Zen::Context]
26
+ def initialize(sink:, context:)
27
+ @sink = sink
28
+ @context = context
29
+ @errors = []
30
+ @performed = false
31
+ end
32
+
33
+ def performed?
34
+ @performed
35
+ end
36
+
37
+ # @return [Boolean] whether this scan detected an Attack.
38
+ def attack?
39
+ @attack != nil
40
+ end
41
+
42
+ # @return [Boolean] whether any errors were caught by this Scan.
43
+ def errors?
44
+ @errors.any?
45
+ end
46
+
47
+ # Runs a block of code, capturing its return value as the potential
48
+ # Attack object (or nil, if safe), and how long it took to run.
49
+ #
50
+ # @yieldreturn [Aikido::Zen::Attack, nil]
51
+ # @return [void]
52
+ def perform
53
+ @performed = true
54
+ started_at = monotonic_time
55
+ @attack = yield
56
+ ensure
57
+ @duration = monotonic_time - started_at
58
+ end
59
+
60
+ # Keep track of exceptions encountered during scanning.
61
+ #
62
+ # @param error [Exception]
63
+ # @param scanner [#call]
64
+ #
65
+ # @return [nil]
66
+ def track_error(error, scanner)
67
+ @errors << {error: error, scanner: scanner}
68
+ nil
69
+ end
70
+
71
+ private def monotonic_time
72
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikido::Zen
4
+ module Scanners
5
+ module PathTraversal
6
+ DANGEROUS_PATH_PARTS = ["../", "..\\"]
7
+
8
+ LINUX_PATH_STARTS = [
9
+ "/bin/",
10
+ "/boot/",
11
+ "/dev/",
12
+ "/etc/",
13
+ "/home/",
14
+ "/init/",
15
+ "/lib/",
16
+ "/media/",
17
+ "/mnt/",
18
+ "/opt/",
19
+ "/proc/",
20
+ "/root/",
21
+ "/run/",
22
+ "/sbin/",
23
+ "/srv/",
24
+ "/sys/",
25
+ "/tmp/",
26
+ "/usr/",
27
+ "/var/"
28
+ ]
29
+
30
+ WINDOWS_PATH_STARTS = ["c:/", "c:\\"]
31
+
32
+ DANGEROUS_PATH_STARTS = LINUX_PATH_STARTS + WINDOWS_PATH_STARTS
33
+
34
+ module Helpers
35
+ def self.include_unsafe_path_parts?(filepath)
36
+ DANGEROUS_PATH_PARTS.each do |dangerous_part|
37
+ return true if filepath.include?(dangerous_part)
38
+ end
39
+
40
+ false
41
+ end
42
+
43
+ def self.start_with_unsafe_path?(filepath, user_input)
44
+ # Check if path is relative (not absolute or drive letter path)
45
+ # Required because `expand_path` will build absolute paths from relative paths
46
+ return false if Pathname.new(filepath).relative? || Pathname.new(user_input).relative?
47
+
48
+ normalized_path = File.expand_path__internal_for_aikido_zen(filepath).downcase
49
+ normalized_user_input = File.expand_path__internal_for_aikido_zen(user_input).downcase
50
+
51
+ DANGEROUS_PATH_STARTS.each do |dangerous_start|
52
+ if normalized_path.start_with?(dangerous_start) && normalized_path.start_with?(normalized_user_input)
53
+ # If the user input is the same as the dangerous start, we don't want to flag it
54
+ # to prevent false positives.
55
+ # e.g., if user input is /etc/ and the path is /etc/passwd, we don't want to flag it,
56
+ # as long as the user input does not contain a subdirectory or filename
57
+ return false if user_input == dangerous_start || user_input == dangerous_start.chomp("/")
58
+
59
+ return true
60
+ end
61
+ end
62
+
63
+ false
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "path_traversal/helpers"
4
+
5
+ module Aikido::Zen
6
+ module Scanners
7
+ class PathTraversalScanner
8
+ def self.skips_on_nil_context?
9
+ true
10
+ end
11
+
12
+ # Checks if the user introduced input is trying to access other path using
13
+ # Path Traversal kind of attacks.
14
+ #
15
+ # @param filepath [String] the expanded path that is tried to be read
16
+ # @param context [Aikido::Zen::Context]
17
+ # @param sink [Aikido::Zen::Sink] the Sink that is running the scan.
18
+ # @param operation [Symbol, String] name of the method being scanned.
19
+ #
20
+ # @return [Aikido::Zen::Attacks::PathTraversalAttack, nil] an Attack if any
21
+ # user input is detected to be attempting a Path Traversal Attack, or +nil+ if not.
22
+ def self.call(filepath:, sink:, context:, operation:)
23
+ context.payloads.each do |payload|
24
+ next unless new(filepath, payload.value.to_s).attack?
25
+
26
+ return Attacks::PathTraversalAttack.new(
27
+ sink: sink,
28
+ input: payload,
29
+ filepath: filepath,
30
+ context: context,
31
+ operation: "#{sink.operation}.#{operation}",
32
+ stack: Aikido::Zen.clean_stack_trace
33
+ )
34
+ end
35
+
36
+ nil
37
+ end
38
+
39
+ def initialize(filepath, input)
40
+ @filepath = filepath.downcase
41
+ @input = input.downcase
42
+ end
43
+
44
+ def attack?
45
+ # Single character are ignored because they don't pose a big threat
46
+ return false if @input.length <= 1
47
+
48
+ # We ignore cases where the user input is longer than the file path.
49
+ # Because the user input can't be part of the file path.
50
+ return false if @input.length > @filepath.length
51
+
52
+ # We ignore cases where the user input is not part of the file path.
53
+ return false unless @filepath.include?(@input)
54
+
55
+ if PathTraversal::Helpers.include_unsafe_path_parts?(@filepath) && PathTraversal::Helpers.include_unsafe_path_parts?(@input)
56
+ return true
57
+ end
58
+
59
+ # Check for absolute path traversal
60
+ PathTraversal::Helpers.start_with_unsafe_path?(@filepath, @input)
61
+ end
62
+ end
63
+ end
64
+ end