aikido-zen 0.1.0.alpha4-arm64-darwin

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) 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/libzen-v0.1.26.aarch64.dylib +0 -0
  22. data/lib/aikido/zen/middleware/check_allowed_addresses.rb +38 -0
  23. data/lib/aikido/zen/middleware/set_context.rb +26 -0
  24. data/lib/aikido/zen/middleware/throttler.rb +50 -0
  25. data/lib/aikido/zen/outbound_connection.rb +45 -0
  26. data/lib/aikido/zen/outbound_connection_monitor.rb +19 -0
  27. data/lib/aikido/zen/package.rb +22 -0
  28. data/lib/aikido/zen/payload.rb +48 -0
  29. data/lib/aikido/zen/rails_engine.rb +53 -0
  30. data/lib/aikido/zen/rate_limiter/breaker.rb +61 -0
  31. data/lib/aikido/zen/rate_limiter/bucket.rb +76 -0
  32. data/lib/aikido/zen/rate_limiter/result.rb +31 -0
  33. data/lib/aikido/zen/rate_limiter.rb +55 -0
  34. data/lib/aikido/zen/request/heuristic_router.rb +109 -0
  35. data/lib/aikido/zen/request/rails_router.rb +84 -0
  36. data/lib/aikido/zen/request/schema/auth_discovery.rb +86 -0
  37. data/lib/aikido/zen/request/schema/auth_schemas.rb +40 -0
  38. data/lib/aikido/zen/request/schema/builder.rb +125 -0
  39. data/lib/aikido/zen/request/schema/definition.rb +112 -0
  40. data/lib/aikido/zen/request/schema/empty_schema.rb +28 -0
  41. data/lib/aikido/zen/request/schema.rb +72 -0
  42. data/lib/aikido/zen/request.rb +97 -0
  43. data/lib/aikido/zen/route.rb +39 -0
  44. data/lib/aikido/zen/runtime_settings/endpoints.rb +49 -0
  45. data/lib/aikido/zen/runtime_settings/ip_set.rb +36 -0
  46. data/lib/aikido/zen/runtime_settings/protection_settings.rb +62 -0
  47. data/lib/aikido/zen/runtime_settings/rate_limit_settings.rb +47 -0
  48. data/lib/aikido/zen/runtime_settings.rb +70 -0
  49. data/lib/aikido/zen/scan.rb +75 -0
  50. data/lib/aikido/zen/scanners/sql_injection_scanner.rb +95 -0
  51. data/lib/aikido/zen/scanners/ssrf/dns_lookups.rb +27 -0
  52. data/lib/aikido/zen/scanners/ssrf/private_ip_checker.rb +85 -0
  53. data/lib/aikido/zen/scanners/ssrf_scanner.rb +251 -0
  54. data/lib/aikido/zen/scanners/stored_ssrf_scanner.rb +43 -0
  55. data/lib/aikido/zen/scanners.rb +5 -0
  56. data/lib/aikido/zen/sink.rb +108 -0
  57. data/lib/aikido/zen/sinks/async_http.rb +63 -0
  58. data/lib/aikido/zen/sinks/curb.rb +89 -0
  59. data/lib/aikido/zen/sinks/em_http.rb +71 -0
  60. data/lib/aikido/zen/sinks/excon.rb +103 -0
  61. data/lib/aikido/zen/sinks/http.rb +76 -0
  62. data/lib/aikido/zen/sinks/httpclient.rb +68 -0
  63. data/lib/aikido/zen/sinks/httpx.rb +61 -0
  64. data/lib/aikido/zen/sinks/mysql2.rb +21 -0
  65. data/lib/aikido/zen/sinks/net_http.rb +85 -0
  66. data/lib/aikido/zen/sinks/patron.rb +88 -0
  67. data/lib/aikido/zen/sinks/pg.rb +50 -0
  68. data/lib/aikido/zen/sinks/resolv.rb +41 -0
  69. data/lib/aikido/zen/sinks/socket.rb +51 -0
  70. data/lib/aikido/zen/sinks/sqlite3.rb +30 -0
  71. data/lib/aikido/zen/sinks/trilogy.rb +21 -0
  72. data/lib/aikido/zen/sinks/typhoeus.rb +78 -0
  73. data/lib/aikido/zen/sinks.rb +21 -0
  74. data/lib/aikido/zen/stats/routes.rb +53 -0
  75. data/lib/aikido/zen/stats/sink_stats.rb +95 -0
  76. data/lib/aikido/zen/stats/users.rb +26 -0
  77. data/lib/aikido/zen/stats.rb +171 -0
  78. data/lib/aikido/zen/synchronizable.rb +24 -0
  79. data/lib/aikido/zen/system_info.rb +84 -0
  80. data/lib/aikido/zen/version.rb +10 -0
  81. data/lib/aikido/zen.rb +138 -0
  82. data/lib/aikido-zen.rb +3 -0
  83. data/lib/aikido.rb +3 -0
  84. data/tasklib/libzen.rake +128 -0
  85. metadata +175 -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