aikido-zen 0.1.0.alpha4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (84) hide show
  1. checksums.yaml +7 -0
  2. data/.ruby-version +1 -0
  3. data/.standard.yml +3 -0
  4. data/CHANGELOG.md +5 -0
  5. data/CODE_OF_CONDUCT.md +132 -0
  6. data/LICENSE +674 -0
  7. data/README.md +40 -0
  8. data/Rakefile +63 -0
  9. data/lib/aikido/zen/actor.rb +116 -0
  10. data/lib/aikido/zen/agent.rb +187 -0
  11. data/lib/aikido/zen/api_client.rb +132 -0
  12. data/lib/aikido/zen/attack.rb +138 -0
  13. data/lib/aikido/zen/capped_collections.rb +68 -0
  14. data/lib/aikido/zen/config.rb +229 -0
  15. data/lib/aikido/zen/context/rack_request.rb +24 -0
  16. data/lib/aikido/zen/context/rails_request.rb +42 -0
  17. data/lib/aikido/zen/context.rb +101 -0
  18. data/lib/aikido/zen/errors.rb +88 -0
  19. data/lib/aikido/zen/event.rb +66 -0
  20. data/lib/aikido/zen/internals.rb +64 -0
  21. data/lib/aikido/zen/middleware/check_allowed_addresses.rb +38 -0
  22. data/lib/aikido/zen/middleware/set_context.rb +26 -0
  23. data/lib/aikido/zen/middleware/throttler.rb +50 -0
  24. data/lib/aikido/zen/outbound_connection.rb +45 -0
  25. data/lib/aikido/zen/outbound_connection_monitor.rb +19 -0
  26. data/lib/aikido/zen/package.rb +22 -0
  27. data/lib/aikido/zen/payload.rb +48 -0
  28. data/lib/aikido/zen/rails_engine.rb +53 -0
  29. data/lib/aikido/zen/rate_limiter/breaker.rb +61 -0
  30. data/lib/aikido/zen/rate_limiter/bucket.rb +76 -0
  31. data/lib/aikido/zen/rate_limiter/result.rb +31 -0
  32. data/lib/aikido/zen/rate_limiter.rb +55 -0
  33. data/lib/aikido/zen/request/heuristic_router.rb +109 -0
  34. data/lib/aikido/zen/request/rails_router.rb +84 -0
  35. data/lib/aikido/zen/request/schema/auth_discovery.rb +86 -0
  36. data/lib/aikido/zen/request/schema/auth_schemas.rb +40 -0
  37. data/lib/aikido/zen/request/schema/builder.rb +125 -0
  38. data/lib/aikido/zen/request/schema/definition.rb +112 -0
  39. data/lib/aikido/zen/request/schema/empty_schema.rb +28 -0
  40. data/lib/aikido/zen/request/schema.rb +72 -0
  41. data/lib/aikido/zen/request.rb +97 -0
  42. data/lib/aikido/zen/route.rb +39 -0
  43. data/lib/aikido/zen/runtime_settings/endpoints.rb +49 -0
  44. data/lib/aikido/zen/runtime_settings/ip_set.rb +36 -0
  45. data/lib/aikido/zen/runtime_settings/protection_settings.rb +62 -0
  46. data/lib/aikido/zen/runtime_settings/rate_limit_settings.rb +47 -0
  47. data/lib/aikido/zen/runtime_settings.rb +70 -0
  48. data/lib/aikido/zen/scan.rb +75 -0
  49. data/lib/aikido/zen/scanners/sql_injection_scanner.rb +95 -0
  50. data/lib/aikido/zen/scanners/ssrf/dns_lookups.rb +27 -0
  51. data/lib/aikido/zen/scanners/ssrf/private_ip_checker.rb +85 -0
  52. data/lib/aikido/zen/scanners/ssrf_scanner.rb +251 -0
  53. data/lib/aikido/zen/scanners/stored_ssrf_scanner.rb +43 -0
  54. data/lib/aikido/zen/scanners.rb +5 -0
  55. data/lib/aikido/zen/sink.rb +108 -0
  56. data/lib/aikido/zen/sinks/async_http.rb +63 -0
  57. data/lib/aikido/zen/sinks/curb.rb +89 -0
  58. data/lib/aikido/zen/sinks/em_http.rb +71 -0
  59. data/lib/aikido/zen/sinks/excon.rb +103 -0
  60. data/lib/aikido/zen/sinks/http.rb +76 -0
  61. data/lib/aikido/zen/sinks/httpclient.rb +68 -0
  62. data/lib/aikido/zen/sinks/httpx.rb +61 -0
  63. data/lib/aikido/zen/sinks/mysql2.rb +21 -0
  64. data/lib/aikido/zen/sinks/net_http.rb +85 -0
  65. data/lib/aikido/zen/sinks/patron.rb +88 -0
  66. data/lib/aikido/zen/sinks/pg.rb +50 -0
  67. data/lib/aikido/zen/sinks/resolv.rb +41 -0
  68. data/lib/aikido/zen/sinks/socket.rb +51 -0
  69. data/lib/aikido/zen/sinks/sqlite3.rb +30 -0
  70. data/lib/aikido/zen/sinks/trilogy.rb +21 -0
  71. data/lib/aikido/zen/sinks/typhoeus.rb +78 -0
  72. data/lib/aikido/zen/sinks.rb +21 -0
  73. data/lib/aikido/zen/stats/routes.rb +53 -0
  74. data/lib/aikido/zen/stats/sink_stats.rb +95 -0
  75. data/lib/aikido/zen/stats/users.rb +26 -0
  76. data/lib/aikido/zen/stats.rb +171 -0
  77. data/lib/aikido/zen/synchronizable.rb +24 -0
  78. data/lib/aikido/zen/system_info.rb +84 -0
  79. data/lib/aikido/zen/version.rb +10 -0
  80. data/lib/aikido/zen.rb +138 -0
  81. data/lib/aikido-zen.rb +3 -0
  82. data/lib/aikido.rb +3 -0
  83. data/tasklib/libzen.rake +128 -0
  84. metadata +171 -0
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ module Aikido::Zen
6
+ # @api private
7
+ #
8
+ # Provides a FIFO set with a maximum size. Adding an element after the
9
+ # capacity has been reached kicks the oldest element in the set out,
10
+ # while maintaining the uniqueness property of a set (relying on #eql?
11
+ # and #hash).
12
+ class CappedSet
13
+ include Enumerable
14
+ extend Forwardable
15
+
16
+ def_delegators :@data, :size, :empty?
17
+
18
+ # @return [Integer]
19
+ attr_reader :capacity
20
+
21
+ def initialize(capacity)
22
+ @data = CappedMap.new(capacity)
23
+ end
24
+
25
+ def <<(element)
26
+ @data[element] = nil
27
+ self
28
+ end
29
+ alias_method :add, :<<
30
+ alias_method :push, :<<
31
+
32
+ def each(&b)
33
+ @data.each_key(&b)
34
+ end
35
+
36
+ def as_json
37
+ map(&:as_json)
38
+ end
39
+ end
40
+
41
+ # @api private
42
+ #
43
+ # Provides a FIFO hash-like structure with a maximum size. Adding a new key
44
+ # after the capacity has been reached kicks the first element pair added out.
45
+ class CappedMap
46
+ include Enumerable
47
+ extend Forwardable
48
+
49
+ def_delegators :@data,
50
+ :[], :fetch, :delete, :key?,
51
+ :each, :each_key, :each_value,
52
+ :size, :empty?, :to_hash
53
+
54
+ # @return [Integer]
55
+ attr_reader :capacity
56
+
57
+ def initialize(capacity)
58
+ raise ArgumentError, "cannot set capacity lower than 1: #{capacity}" if capacity < 1
59
+ @capacity = capacity
60
+ @data = {}
61
+ end
62
+
63
+ def []=(key, value)
64
+ @data[key] = value
65
+ @data.delete(@data.each_key.first) if @data.size > @capacity
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,229 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+ require "json"
5
+ require "logger"
6
+
7
+ require_relative "context"
8
+
9
+ module Aikido::Zen
10
+ class Config
11
+ # @return [Boolean] whether Aikido should only report infractions or block
12
+ # the request by raising an Exception. Defaults to whether AIKIDO_BLOCKING
13
+ # is set to a non-empty value in your environment, or +false+ otherwise.
14
+ attr_accessor :blocking_mode
15
+ alias_method :blocking_mode?, :blocking_mode
16
+
17
+ # @return [URI] The HTTP host for the Aikido API. Defaults to
18
+ # +https://guard.aikido.dev+.
19
+ attr_reader :api_base_url
20
+
21
+ # @return [URI] The HTTP host for the Aikido Runtime API. Defaults to
22
+ # +https://runtime.aikido.dev+.
23
+ attr_reader :runtime_api_base_url
24
+
25
+ # @return [Hash] HTTP timeouts for communicating with the API.
26
+ attr_reader :api_timeouts
27
+
28
+ # @return [String] the token obtained when configuring the Firewall in the
29
+ # Aikido interface.
30
+ attr_accessor :api_token
31
+
32
+ # @return [Integer] the interval in seconds to poll the runtime API for
33
+ # settings changes. Defaults to evey 60 seconds.
34
+ attr_accessor :polling_interval
35
+
36
+ # @return [Integer] the amount in seconds to wait before sending an initial
37
+ # heartbeat event when the server reports no stats have been sent yet.
38
+ attr_accessor :initial_heartbeat_delay
39
+
40
+ # @return [#call] Callable that can be passed an Object and returns a String
41
+ # of JSON. Defaults to the standard library's JSON.dump method.
42
+ attr_accessor :json_encoder
43
+
44
+ # @return [#call] Callable that can be passed a JSON string and parses it
45
+ # into an Object. Defaults to the standard library's JSON.parse method.
46
+ attr_accessor :json_decoder
47
+
48
+ # @returns [Logger]
49
+ attr_accessor :logger
50
+
51
+ # @return [Integer] maximum number of timing measurements to keep in memory
52
+ # before compressing them.
53
+ attr_accessor :max_performance_samples
54
+
55
+ # @return [Integer] maximum number of compressed performance samples to keep
56
+ # in memory. If we take more than this before reporting them to Aikido, we
57
+ # will discard the oldest samples.
58
+ attr_accessor :max_compressed_stats
59
+
60
+ # @return [Integer] maximum number of connections to outbound hosts to keep
61
+ # in memory in order to report them in the next heartbeat event. If new
62
+ # connections are added to the set before reporting them to Aikido, we
63
+ # will discard the oldest data point.
64
+ attr_accessor :max_outbound_connections
65
+
66
+ # @return [Integer] maximum number of users tracked via Zen.track_user to
67
+ # share with the Aikido servers on the next heartbeat event. If more
68
+ # unique users (by their ID) are tracked than this number, we will discard
69
+ # the oldest seen users.
70
+ attr_accessor :max_users_tracked
71
+
72
+ # @return [Proc{Aikido::Zen::Request => Array(Integer, Hash, #each)}]
73
+ # Rack handler used to respond to requests from IPs blocked in the Aikido
74
+ # dashboard.
75
+ attr_accessor :blocked_ip_responder
76
+
77
+ # @return [Proc{Aikido::Zen::Request => Array(Integer, Hash, #each)}]
78
+ # Rack handler used to respond to requests that have been rate limited.
79
+ attr_accessor :rate_limited_responder
80
+
81
+ # @return [Proc{Aikido::Zen::Request => String}] a proc that reads
82
+ # information off the current request and returns a String to
83
+ # differentiate different clients. By default this uses the request IP.
84
+ attr_accessor :rate_limiting_discriminator
85
+
86
+ # @return [Boolean] whether Zen should infer the schema from request bodies
87
+ # sent to the app. Defaults to +false+ or the value of the environment
88
+ # variable AIKIDO_FEATURE_COLLECT_API_SCHEMA.
89
+ attr_accessor :api_schema_collection_enabled
90
+ alias_method :api_schema_collection_enabled?, :api_schema_collection_enabled
91
+
92
+ # @api private
93
+ # @return [Integer] max number of levels deep we want to read a nested
94
+ # strcture for performance reasons.
95
+ attr_accessor :api_schema_collection_max_depth
96
+
97
+ # @api private
98
+ # @return [Integer] max number of properties that we want to inspect per
99
+ # level of the structure for performance reasons.
100
+ attr_accessor :api_schema_collection_max_properties
101
+
102
+ # @api private
103
+ # @return [Proc<Hash => Aikido::Zen::Context>] callable that takes a
104
+ # Rack-compatible env Hash and returns a Context object with an HTTP
105
+ # request. This is meant to be overridden by each framework adapter.
106
+ attr_accessor :request_builder
107
+
108
+ # @api private
109
+ # @return [Integer] number of seconds to perform client-side rate limiting
110
+ # of events sent to the server.
111
+ attr_accessor :client_rate_limit_period
112
+
113
+ # @api private
114
+ # @return [Integer] max number of events sent during a sliding
115
+ # {client_rate_limit_period} window.
116
+ attr_accessor :client_rate_limit_max_events
117
+
118
+ # @api private
119
+ # @return [Integer] number of seconds to wait before sending an event after
120
+ # the server returns a 429 response.
121
+ attr_accessor :server_rate_limit_deadline
122
+
123
+ # @return [Array<String>] when checking for stored SSRF attacks, we want to
124
+ # allow known hosts that should be able to resolve to the IMDS service.
125
+ attr_accessor :imds_allowed_hosts
126
+
127
+ def initialize
128
+ self.blocking_mode = !!ENV.fetch("AIKIDO_BLOCKING", false)
129
+ self.api_timeouts = 10
130
+ self.api_base_url = ENV.fetch("AIKIDO_BASE_URL", DEFAULT_API_BASE_URL)
131
+ self.runtime_api_base_url = ENV.fetch("AIKIDO_RUNTIME_URL", DEFAULT_RUNTIME_BASE_URL)
132
+ self.api_token = ENV.fetch("AIKIDO_TOKEN", nil)
133
+ self.polling_interval = 60
134
+ self.initial_heartbeat_delay = 60
135
+ self.json_encoder = DEFAULT_JSON_ENCODER
136
+ self.json_decoder = DEFAULT_JSON_DECODER
137
+ self.logger = Logger.new($stdout, progname: "aikido")
138
+ self.max_performance_samples = 5000
139
+ self.max_compressed_stats = 100
140
+ self.max_outbound_connections = 200
141
+ self.max_users_tracked = 1000
142
+ self.request_builder = Aikido::Zen::Context::RACK_REQUEST_BUILDER
143
+ self.blocked_ip_responder = DEFAULT_BLOCKED_IP_RESPONDER
144
+ self.rate_limited_responder = DEFAULT_RATE_LIMITED_RESPONDER
145
+ self.rate_limiting_discriminator = DEFAULT_RATE_LIMITING_DISCRIMINATOR
146
+ self.server_rate_limit_deadline = 1800 # 30 min
147
+ self.client_rate_limit_period = 3600 # 1 hour
148
+ self.client_rate_limit_max_events = 100
149
+
150
+ self.api_schema_collection_enabled = read_boolean_from_env(ENV.fetch("AIKIDO_FEATURE_COLLECT_API_SCHEMA", false))
151
+ self.api_schema_collection_max_depth = 20
152
+ self.api_schema_collection_max_properties = 20
153
+
154
+ self.imds_allowed_hosts = ["metadata.google.internal", "metadata.goog"]
155
+ end
156
+
157
+ # Set the base URL for API requests.
158
+ #
159
+ # @param url [String, URI]
160
+ def api_base_url=(url)
161
+ @api_base_url = URI(url)
162
+ end
163
+
164
+ # Set the base URL for runtime API requests.
165
+ #
166
+ # @param url [String, URI]
167
+ def runtime_api_base_url=(url)
168
+ @runtime_api_base_url = URI(url)
169
+ end
170
+
171
+ # @overload def api_timeouts=(timeouts)
172
+ # Configure granular connection timeouts for the Aikido Zen API. You
173
+ # can set any of these per call.
174
+ # @param timeouts [Hash]
175
+ # @option timeouts [Integer] :open_timeout Duration in seconds.
176
+ # @option timeouts [Integer] :read_timeout Duration in seconds.
177
+ # @option timeouts [Integer] :write_timeout Duration in seconds.
178
+ #
179
+ # @overload def api_timeouts=(duration)
180
+ # Configure the connection timeouts for the Aikido Zen API.
181
+ # @param duration [Integer] Duration in seconds to set for all three
182
+ # timeouts (open, read, and write).
183
+ def api_timeouts=(value)
184
+ value = {open_timeout: value, read_timeout: value, write_timeout: value} if value.respond_to?(:to_int)
185
+
186
+ @api_timeouts ||= {}
187
+ @api_timeouts.update(value)
188
+ end
189
+
190
+ private
191
+
192
+ def read_boolean_from_env(value)
193
+ return value unless value.respond_to?(:to_str)
194
+
195
+ case value.to_str.strip
196
+ when "false", "", "0", "f"
197
+ false
198
+ else
199
+ true
200
+ end
201
+ end
202
+
203
+ # @!visibility private
204
+ DEFAULT_API_BASE_URL = "https://guard.aikido.dev"
205
+
206
+ # @!visibility private
207
+ DEFAULT_RUNTIME_BASE_URL = "https://runtime.aikido.dev"
208
+
209
+ # @!visibility private
210
+ DEFAULT_JSON_ENCODER = JSON.method(:dump)
211
+
212
+ # @!visibility private
213
+ DEFAULT_JSON_DECODER = JSON.method(:parse)
214
+
215
+ # @!visibility private
216
+ DEFAULT_BLOCKED_IP_RESPONDER = ->(request) do
217
+ message = "Your IP address is not allowed to access this resource. (Your IP: %s)"
218
+ [403, {"Content-Type" => "text/plain"}, [format(message, request.ip)]]
219
+ end
220
+
221
+ # @!visibility private
222
+ DEFAULT_RATE_LIMITED_RESPONDER = ->(request) do
223
+ [429, {"Content-Type" => "text/plain"}, ["Too many requests."]]
224
+ end
225
+
226
+ # @!visibility private
227
+ DEFAULT_RATE_LIMITING_DISCRIMINATOR = ->(request) { request.ip }
228
+ end
229
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../request"
4
+ require_relative "../request/heuristic_router"
5
+
6
+ module Aikido::Zen
7
+ # @!visibility private
8
+ Context::RACK_REQUEST_BUILDER = ->(env) do
9
+ delegate = Rack::Request.new(env)
10
+ router = Aikido::Zen::Request::HeuristicRouter.new
11
+ request = Aikido::Zen::Request.new(delegate, framework: "rack", router: router)
12
+
13
+ Context.new(request) do |req|
14
+ {
15
+ query: req.GET,
16
+ body: req.POST,
17
+ route: {},
18
+ header: req.normalized_headers,
19
+ cookie: req.cookies,
20
+ subdomain: []
21
+ }
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../request"
4
+ require_relative "../request/rails_router"
5
+
6
+ module Aikido::Zen
7
+ module Rails
8
+ def self.router
9
+ @router ||= Request::RailsRouter.new(::Rails.application.routes)
10
+ end
11
+ end
12
+
13
+ # @!visibility private
14
+ Context::RAILS_REQUEST_BUILDER = ->(env) do
15
+ delegate = ActionDispatch::Request.new(env)
16
+ request = Aikido::Zen::Request.new(
17
+ delegate, framework: "rails", router: Rails.router
18
+ )
19
+
20
+ decrypt_cookies = ->(req) do
21
+ return req.cookies unless req.respond_to?(:cookie_jar)
22
+
23
+ req.cookie_jar.map { |key, value|
24
+ plain_text = req.cookie_jar.encrypted[key].presence ||
25
+ req.cookie_jar.signed[key].presence ||
26
+ value
27
+ [key, plain_text]
28
+ }.to_h
29
+ end
30
+
31
+ Context.new(request) do |req|
32
+ {
33
+ query: req.query_parameters,
34
+ body: req.request_parameters,
35
+ route: req.path_parameters,
36
+ header: req.normalized_headers,
37
+ cookie: decrypt_cookies.call(req),
38
+ subdomain: req.subdomains
39
+ }
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ require_relative "request"
6
+ require_relative "payload"
7
+
8
+ module Aikido::Zen
9
+ class Context
10
+ def self.from_rack_env(env, config = Aikido::Zen.config)
11
+ config.request_builder.call(env)
12
+ end
13
+
14
+ attr_reader :request
15
+
16
+ # @param [Rack::Request] a Request object that implements the
17
+ # Rack::Request API, to which we will delegate behavior.
18
+ # @param settings [Aikido::Zen::RuntimeSettings]
19
+ #
20
+ # @yieldparam request [Rack::Request] the given request object.
21
+ # @yieldreturn [Hash<Symbol, #flat_map>] map of payload source types
22
+ # to the actual data from the request to populate them.
23
+ def initialize(request, settings: Aikido::Zen.runtime_settings, &sources)
24
+ @request = request
25
+ @settings = settings
26
+ @payload_sources = sources
27
+ @metadata = {}
28
+ end
29
+
30
+ # Fetch some metadata stored in the Context.
31
+ #
32
+ # @param key [String]
33
+ # @return [Object, nil]
34
+ def [](key)
35
+ @metadata[key]
36
+ end
37
+
38
+ # Store some metadata in the Context so other Scanners can use it.
39
+ #
40
+ # @param key [String]
41
+ # @param value [Object]
42
+ # @return [void]
43
+ def []=(key, value)
44
+ @metadata[key] = value
45
+ end
46
+
47
+ # Overrides the current request, and invalidates any memoized data obtained
48
+ # from it. This is useful for scenarios where setting the request in the
49
+ # middleware isn't enough, such as Rails, where the router modifies it after
50
+ # the middleware has seen it.
51
+ #
52
+ # @param new_request [Rack::Request]
53
+ # @return [void]
54
+ def update_request(new_request)
55
+ @payloads = nil
56
+ request.__setobj__(new_request)
57
+ end
58
+
59
+ # @return [Array<Aikido::Zen::Payload>] list of user inputs from all the
60
+ # different sources we recognize.
61
+ def payloads
62
+ @payloads ||= payload_sources.flat_map do |source, data|
63
+ extract_payloads_from(data, source)
64
+ end
65
+ end
66
+
67
+ # @return [Boolean] whether attack protection for the currently requested
68
+ # endpoint was disabled on the Aikido dashboard, or if the source IP for
69
+ # this request is in the "Bypass List".
70
+ def protection_disabled?
71
+ return false if request.nil?
72
+
73
+ !@settings.endpoints[request.route].protected? ||
74
+ @settings.skip_protection_for_ips.include?(request.ip)
75
+ end
76
+
77
+ # @!visibility private
78
+ def payload_sources
79
+ @payload_sources.call(request)
80
+ end
81
+
82
+ private
83
+
84
+ def extract_payloads_from(data, source_type, prefix = nil)
85
+ if data.respond_to?(:to_hash)
86
+ data.to_hash.flat_map { |name, val|
87
+ extract_payloads_from(val, source_type, [prefix, name].compact.join("."))
88
+ }
89
+ elsif data.respond_to?(:to_ary)
90
+ data.to_ary.flat_map.with_index { |val, idx|
91
+ extract_payloads_from(val, source_type, [prefix, idx].compact.join("."))
92
+ }
93
+ else
94
+ Payload.new(data, source_type, prefix.to_s)
95
+ end
96
+ end
97
+ end
98
+ end
99
+
100
+ require_relative "context/rack_request"
101
+ require_relative "context/rails_request"
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ module Aikido
6
+ # Support rescuing Aikido::Error without forcing a single base class to all
7
+ # errors (so things that should be e.g. a TypeError, can have the correct
8
+ # superclass).
9
+ module Error; end
10
+
11
+ # Generic error for problems with the Agent.
12
+ class ZenError < RuntimeError
13
+ include Error
14
+ end
15
+
16
+ module Zen
17
+ # Wrapper for all low-level network errors communicating with the API. You
18
+ # can access the original error by calling #cause.
19
+ class NetworkError < StandardError
20
+ include Error
21
+
22
+ def initialize(request, cause = nil)
23
+ @request = request.dup
24
+
25
+ super("Error in #{request.method} #{request.path}: #{cause.message}")
26
+ end
27
+ end
28
+
29
+ # Raised whenever a request to the API results in a 4XX or 5XX response.
30
+ class APIError < StandardError
31
+ include Error
32
+
33
+ attr_reader :request
34
+ attr_reader :response
35
+
36
+ def initialize(request, response)
37
+ @request = anonimize_token(request.dup)
38
+ @response = response
39
+
40
+ super("Error in #{request.method} #{request.path}: #{response.code} #{response.message} (#{response.body})")
41
+ end
42
+
43
+ private def anonimize_token(request)
44
+ # Anonimize the token to `********************xxxx`,
45
+ # mimicking what we show in the dashbaord.
46
+ request["Authorization"] = request["Authorization"].to_s
47
+ .gsub(/\A.*(.{4})\z/, ("*" * 20) + "\\1")
48
+ request
49
+ end
50
+ end
51
+
52
+ # Raised whenever a response to the API results in a 429 response.
53
+ class RateLimitedError < APIError; end
54
+
55
+ class UnderAttackError < StandardError
56
+ include Error
57
+
58
+ attr_reader :attack
59
+
60
+ def initialize(attack)
61
+ super(attack.log_message)
62
+ @attack = attack
63
+ end
64
+ end
65
+
66
+ class SQLInjectionError < UnderAttackError
67
+ extend Forwardable
68
+ def_delegators :@attack, :query, :input, :dialect
69
+ end
70
+
71
+ class SSRFDetectedError < UnderAttackError
72
+ extend Forwardable
73
+ def_delegators :@attack, :request, :input
74
+ end
75
+
76
+ # Raised when there's any problem communicating (or loading) libzen.
77
+ class InternalsError < ZenError
78
+ # @param attempt [String] description of what we were trying to do.
79
+ # @param problem [String] what couldn't be done.
80
+ # @param libname [String] the name of the file (including the arch).
81
+ def initialize(attempt, problem, libname)
82
+ super(format(<<~MSG.chomp, attempt, problem, libname))
83
+ Zen could not scan %s due to a problem %s the library `%s'
84
+ MSG
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,66 @@
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
+ attack: @attack.as_json,
45
+ request: @attack.context.request.as_json
46
+ )
47
+ end
48
+ end
49
+
50
+ class Heartbeat < Event
51
+ def initialize(stats:, **opts)
52
+ super(type: "heartbeat", **opts)
53
+ @stats = stats
54
+ end
55
+
56
+ def as_json
57
+ super.update(
58
+ stats: @stats.as_json,
59
+ routes: @stats.routes.as_json,
60
+ hostnames: @stats.outbound_connections.as_json,
61
+ users: @stats.users.as_json
62
+ )
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,64 @@
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
+ class << self
11
+ # @return [String] the name of the extension we're loading, which we can
12
+ # use in error messages to identify the architecture.
13
+ attr_accessor :libzen_name
14
+ end
15
+
16
+ self.libzen_name = [
17
+ "libzen-v#{LIBZEN_VERSION}",
18
+ FFI::Platform::ARCH,
19
+ FFI::Platform::LIBSUFFIX
20
+ ].join(".")
21
+
22
+ begin
23
+ ffi_lib File.expand_path(libzen_name, __dir__)
24
+
25
+ # @!method self.detect_sql_injection_native(query, input, dialect)
26
+ # @param (see .detect_sql_injection)
27
+ # @returns [Integer] 0 if no injection detected, 1 if an injection was
28
+ # detected, or 2 if there was an internal error.
29
+ # @raise [Aikido::Zen::InternalsError] if there's a problem loading or
30
+ # calling libzen.
31
+ attach_function :detect_sql_injection_native, :detect_sql_injection,
32
+ [:string, :string, :int], :int
33
+ rescue LoadError, FFI::NotFoundError => err
34
+ # Emit an $stderr warning at startup.
35
+ warn "Zen could not load its binary extension #{libzen_name}: #{err}"
36
+
37
+ def self.detect_sql_injection(query, *)
38
+ attempt = format("%p for SQL injection", query)
39
+ raise InternalsError.new(attempt, "loading", Internals.libzen_name)
40
+ end
41
+ else
42
+ # Analyzes the SQL query to detect if the provided user input is being
43
+ # passed as-is without escaping.
44
+ #
45
+ # @param query [String]
46
+ # @param input [String]
47
+ # @param dialect [Integer, #to_int] the SQL Dialect identifier in libzen.
48
+ # See {Aikido::Zen::Scanners::SQLInjectionScanner::DIALECTS}.
49
+ #
50
+ # @returns [Boolean]
51
+ # @raise [Aikido::Zen::InternalsError] if there's a problem loading or
52
+ # calling libzen.
53
+ def self.detect_sql_injection(query, input, dialect)
54
+ case detect_sql_injection_native(query, input, dialect)
55
+ when 0 then false
56
+ when 1 then true
57
+ when 2
58
+ attempt = format("%s query %p with input %p", dialect, query, input)
59
+ raise InternalsError.new(attempt, "calling detect_sql_injection in", libzen_name)
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end