aikido-zen 1.0.2.beta.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.
- checksums.yaml +7 -0
- data/.aikido +6 -0
- data/.ruby-version +1 -0
- data/.simplecov +26 -0
- data/.standard.yml +3 -0
- data/LICENSE +674 -0
- data/README.md +146 -0
- data/Rakefile +67 -0
- data/benchmarks/README.md +23 -0
- data/benchmarks/rails7.1_sql_injection.js +70 -0
- data/docs/banner.svg +202 -0
- data/docs/config.md +125 -0
- data/docs/proxy.md +10 -0
- data/docs/rails.md +114 -0
- data/lib/aikido/zen/actor.rb +116 -0
- data/lib/aikido/zen/agent/heartbeats_manager.rb +66 -0
- data/lib/aikido/zen/agent.rb +179 -0
- data/lib/aikido/zen/api_client.rb +145 -0
- data/lib/aikido/zen/attack.rb +207 -0
- data/lib/aikido/zen/background_worker.rb +52 -0
- data/lib/aikido/zen/capped_collections.rb +68 -0
- data/lib/aikido/zen/collector/hosts.rb +15 -0
- data/lib/aikido/zen/collector/routes.rb +66 -0
- data/lib/aikido/zen/collector/sink_stats.rb +95 -0
- data/lib/aikido/zen/collector/stats.rb +111 -0
- data/lib/aikido/zen/collector/users.rb +30 -0
- data/lib/aikido/zen/collector.rb +144 -0
- data/lib/aikido/zen/config.rb +282 -0
- data/lib/aikido/zen/context/rack_request.rb +24 -0
- data/lib/aikido/zen/context/rails_request.rb +44 -0
- data/lib/aikido/zen/context.rb +112 -0
- data/lib/aikido/zen/detached_agent/agent.rb +78 -0
- data/lib/aikido/zen/detached_agent/front_object.rb +37 -0
- data/lib/aikido/zen/detached_agent/server.rb +78 -0
- data/lib/aikido/zen/detached_agent.rb +2 -0
- data/lib/aikido/zen/errors.rb +107 -0
- data/lib/aikido/zen/event.rb +71 -0
- data/lib/aikido/zen/internals.rb +103 -0
- data/lib/aikido/zen/libzen-v0.1.39-aarch64-linux.so +0 -0
- data/lib/aikido/zen/middleware/check_allowed_addresses.rb +26 -0
- data/lib/aikido/zen/middleware/middleware.rb +11 -0
- data/lib/aikido/zen/middleware/rack_throttler.rb +48 -0
- data/lib/aikido/zen/middleware/request_tracker.rb +192 -0
- data/lib/aikido/zen/middleware/set_context.rb +26 -0
- data/lib/aikido/zen/outbound_connection.rb +45 -0
- data/lib/aikido/zen/outbound_connection_monitor.rb +23 -0
- data/lib/aikido/zen/package.rb +22 -0
- data/lib/aikido/zen/payload.rb +50 -0
- data/lib/aikido/zen/rails_engine.rb +56 -0
- data/lib/aikido/zen/rate_limiter/breaker.rb +61 -0
- data/lib/aikido/zen/rate_limiter/bucket.rb +76 -0
- data/lib/aikido/zen/rate_limiter/result.rb +31 -0
- data/lib/aikido/zen/rate_limiter.rb +50 -0
- data/lib/aikido/zen/request/heuristic_router.rb +115 -0
- data/lib/aikido/zen/request/rails_router.rb +77 -0
- data/lib/aikido/zen/request/schema/auth_discovery.rb +86 -0
- data/lib/aikido/zen/request/schema/auth_schemas.rb +54 -0
- data/lib/aikido/zen/request/schema/builder.rb +121 -0
- data/lib/aikido/zen/request/schema/definition.rb +107 -0
- data/lib/aikido/zen/request/schema/empty_schema.rb +28 -0
- data/lib/aikido/zen/request/schema.rb +87 -0
- data/lib/aikido/zen/request.rb +122 -0
- data/lib/aikido/zen/route.rb +39 -0
- data/lib/aikido/zen/runtime_settings/endpoints.rb +49 -0
- data/lib/aikido/zen/runtime_settings/ip_set.rb +36 -0
- data/lib/aikido/zen/runtime_settings/protection_settings.rb +62 -0
- data/lib/aikido/zen/runtime_settings/rate_limit_settings.rb +47 -0
- data/lib/aikido/zen/runtime_settings.rb +65 -0
- data/lib/aikido/zen/scan.rb +75 -0
- data/lib/aikido/zen/scanners/path_traversal/helpers.rb +65 -0
- data/lib/aikido/zen/scanners/path_traversal_scanner.rb +63 -0
- data/lib/aikido/zen/scanners/shell_injection/helpers.rb +159 -0
- data/lib/aikido/zen/scanners/shell_injection_scanner.rb +64 -0
- data/lib/aikido/zen/scanners/sql_injection_scanner.rb +93 -0
- data/lib/aikido/zen/scanners/ssrf/dns_lookups.rb +27 -0
- data/lib/aikido/zen/scanners/ssrf/private_ip_checker.rb +97 -0
- data/lib/aikido/zen/scanners/ssrf_scanner.rb +265 -0
- data/lib/aikido/zen/scanners/stored_ssrf_scanner.rb +49 -0
- data/lib/aikido/zen/scanners.rb +7 -0
- data/lib/aikido/zen/sink.rb +118 -0
- data/lib/aikido/zen/sinks/action_controller.rb +83 -0
- data/lib/aikido/zen/sinks/async_http.rb +80 -0
- data/lib/aikido/zen/sinks/curb.rb +113 -0
- data/lib/aikido/zen/sinks/em_http.rb +83 -0
- data/lib/aikido/zen/sinks/excon.rb +118 -0
- data/lib/aikido/zen/sinks/file.rb +112 -0
- data/lib/aikido/zen/sinks/http.rb +93 -0
- data/lib/aikido/zen/sinks/httpclient.rb +95 -0
- data/lib/aikido/zen/sinks/httpx.rb +78 -0
- data/lib/aikido/zen/sinks/kernel.rb +33 -0
- data/lib/aikido/zen/sinks/mysql2.rb +31 -0
- data/lib/aikido/zen/sinks/net_http.rb +101 -0
- data/lib/aikido/zen/sinks/patron.rb +103 -0
- data/lib/aikido/zen/sinks/pg.rb +72 -0
- data/lib/aikido/zen/sinks/resolv.rb +62 -0
- data/lib/aikido/zen/sinks/socket.rb +78 -0
- data/lib/aikido/zen/sinks/sqlite3.rb +46 -0
- data/lib/aikido/zen/sinks/trilogy.rb +31 -0
- data/lib/aikido/zen/sinks/typhoeus.rb +78 -0
- data/lib/aikido/zen/sinks.rb +36 -0
- data/lib/aikido/zen/sinks_dsl.rb +250 -0
- data/lib/aikido/zen/synchronizable.rb +24 -0
- data/lib/aikido/zen/system_info.rb +84 -0
- data/lib/aikido/zen/version.rb +10 -0
- data/lib/aikido/zen/worker.rb +87 -0
- data/lib/aikido/zen.rb +246 -0
- data/lib/aikido-zen.rb +3 -0
- data/placeholder/.gitignore +4 -0
- data/placeholder/README.md +11 -0
- data/placeholder/Rakefile +75 -0
- data/placeholder/lib/placeholder.rb.template +3 -0
- data/placeholder/placeholder.gemspec.template +20 -0
- data/tasklib/bench.rake +94 -0
- data/tasklib/libzen.rake +133 -0
- data/tasklib/wrk.rb +88 -0
- metadata +205 -0
data/docs/rails.md
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
# Setting up Zen on a Ruby on Rails application
|
2
|
+
|
3
|
+
To install Zen, add the gem:
|
4
|
+
|
5
|
+
```sh
|
6
|
+
bundle add aikido-zen
|
7
|
+
```
|
8
|
+
|
9
|
+
And require it before `Bundler.require` in `config/application.rb`:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
# config/application.rb
|
13
|
+
require_relative "boot"
|
14
|
+
|
15
|
+
require "rails/all"
|
16
|
+
|
17
|
+
require "aikido-zen"
|
18
|
+
Aikido::Zen.protect!
|
19
|
+
|
20
|
+
# Require the gems listed in Gemfile, including any gems
|
21
|
+
# you've limited to :test, :development, or :production.
|
22
|
+
Bundler.require(*Rails.groups)
|
23
|
+
|
24
|
+
...
|
25
|
+
```
|
26
|
+
|
27
|
+
That's it! Zen will start to run inside your app when it starts getting
|
28
|
+
requests.
|
29
|
+
|
30
|
+
## Rate limiting and user blocking
|
31
|
+
|
32
|
+
If you want to add the rate limiting feature to your app, modify your code like this:
|
33
|
+
|
34
|
+
```ruby
|
35
|
+
# app/controllers/application_controller.rb
|
36
|
+
class ApplicationController < ActionController::Base
|
37
|
+
private
|
38
|
+
|
39
|
+
def current_user
|
40
|
+
return unless session[:user_id]
|
41
|
+
User.find(session[:user_id])
|
42
|
+
end
|
43
|
+
|
44
|
+
def authenticate_user!
|
45
|
+
# Your authentication logic here
|
46
|
+
# ...
|
47
|
+
# Optional, if you want to use user based rate limiting or block specific users
|
48
|
+
Aikido::Zen.set_user(
|
49
|
+
id: current_user.id,
|
50
|
+
name: current_user.name
|
51
|
+
)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
```
|
55
|
+
|
56
|
+
## Configuration
|
57
|
+
|
58
|
+
Zen exposes its configuration object to the Rails configuration, which you can
|
59
|
+
modify in an initializer if desired:
|
60
|
+
|
61
|
+
```ruby
|
62
|
+
# config/initializers/zen.rb
|
63
|
+
Rails.application.config.zen.api_timeouts = 20
|
64
|
+
```
|
65
|
+
|
66
|
+
You can access the configuration object both as `Aikido::Zen.config` or
|
67
|
+
`Rails.configuration.zen`.
|
68
|
+
|
69
|
+
See our [configuration guide](./config.md) for more details.
|
70
|
+
|
71
|
+
## Using Rails encrypted credentials
|
72
|
+
|
73
|
+
If you're using Rails' [encrypted credentials][creds], and prefer not storing
|
74
|
+
sensitive values in your env vars, you can easily configure Zen for it. For
|
75
|
+
example, assuming the following credentials structure:
|
76
|
+
|
77
|
+
```yaml
|
78
|
+
# config/credentials.yml.enc
|
79
|
+
zen:
|
80
|
+
token: "AIKIDO_RUNTIME_..."
|
81
|
+
```
|
82
|
+
|
83
|
+
You can just tell Zen to use it like so:
|
84
|
+
|
85
|
+
```ruby
|
86
|
+
# config/initializers/zen.rb
|
87
|
+
Rails.application.config.zen.token = Rails.application.credentials.zen.token
|
88
|
+
```
|
89
|
+
|
90
|
+
[creds]: https://guides.rubyonrails.org/security.html#environmental-security
|
91
|
+
|
92
|
+
## Blocking mode
|
93
|
+
|
94
|
+
By default, Zen will only detect and log attacks, but will not block them. You
|
95
|
+
can enable blocking mode by setting the `AIKIDO_BLOCK` environment variable
|
96
|
+
to `true`.
|
97
|
+
|
98
|
+
When in blocking mode, Zen will raise an exception when it detects an attack.
|
99
|
+
These exceptions depend on the type of attack, but all inherit from
|
100
|
+
`Aikido::Zen::UnderAttackError`, if you wish to handle these exceptions in any
|
101
|
+
way.
|
102
|
+
|
103
|
+
## Logging
|
104
|
+
|
105
|
+
By default, Zen will use the Rails logger, prefixing messages with `[aikido]`.
|
106
|
+
You can redirect the log to a separate stream by overriding the logger:
|
107
|
+
|
108
|
+
```ruby
|
109
|
+
# config/initializers/zen.rb
|
110
|
+
Rails.application.config.zen.logger = Logger.new(...)
|
111
|
+
```
|
112
|
+
|
113
|
+
You should supply an instance of ruby's [Logger](https://github.com/ruby/logger)
|
114
|
+
class.
|
@@ -0,0 +1,116 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "concurrent"
|
4
|
+
require_relative "config"
|
5
|
+
|
6
|
+
module Aikido::Zen
|
7
|
+
# Converts an object into an Actor for reporting back to the Aikido Dashboard.
|
8
|
+
#
|
9
|
+
# @overload Actor(actor)
|
10
|
+
# @param actor [#to_aikido_actor] anything that implements #to_aikido_actor
|
11
|
+
# will have that method called and its value returned.
|
12
|
+
# @return Aikido::Zen::Actor
|
13
|
+
#
|
14
|
+
# @overload Actor(data)
|
15
|
+
# @param data [Hash<Symbol, String>]
|
16
|
+
# @option data [String] :id a unique identifier for this user.
|
17
|
+
# @option data [String, nil] :name an optional name to display in the UI.
|
18
|
+
# @return Aikido::Zen::Actor
|
19
|
+
def self.Actor(data)
|
20
|
+
return if data.nil?
|
21
|
+
return data.to_aikido_actor if data.respond_to?(:to_aikido_actor)
|
22
|
+
|
23
|
+
attrs = {}
|
24
|
+
if data.respond_to?(:to_hash)
|
25
|
+
attrs = data.to_hash
|
26
|
+
.slice("id", "name", :id, :name)
|
27
|
+
.compact
|
28
|
+
.transform_keys(&:to_sym)
|
29
|
+
.transform_values(&:to_s)
|
30
|
+
else
|
31
|
+
return nil
|
32
|
+
end
|
33
|
+
|
34
|
+
return nil if attrs[:id].nil? || attrs[:id].to_s.strip.empty?
|
35
|
+
|
36
|
+
Actor.new(**attrs)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Represents someone connecting to the application and making requests.
|
40
|
+
class Actor
|
41
|
+
# @return [String] a unique identifier for this user.
|
42
|
+
attr_reader :id
|
43
|
+
|
44
|
+
# @return [String, nil] an optional name to display in the Aikido UI.
|
45
|
+
attr_reader :name
|
46
|
+
|
47
|
+
# @return [Time]
|
48
|
+
attr_reader :first_seen_at
|
49
|
+
|
50
|
+
# @param id [String]
|
51
|
+
# @param name [String, nil]
|
52
|
+
# @param ip [String, nil]
|
53
|
+
# @param seen_at [Time]
|
54
|
+
def initialize(
|
55
|
+
id:,
|
56
|
+
name: nil,
|
57
|
+
ip: Aikido::Zen.current_context&.request&.ip,
|
58
|
+
seen_at: Time.now.utc
|
59
|
+
)
|
60
|
+
@id = id
|
61
|
+
@name = name
|
62
|
+
@first_seen_at = seen_at
|
63
|
+
@last_seen_at = Concurrent::AtomicReference.new(seen_at)
|
64
|
+
@ip = Concurrent::AtomicReference.new(ip)
|
65
|
+
end
|
66
|
+
|
67
|
+
# @return [Time]
|
68
|
+
def last_seen_at
|
69
|
+
@last_seen_at.get
|
70
|
+
end
|
71
|
+
|
72
|
+
# @return [String, nil] the IP address last used by this user, if available.
|
73
|
+
def ip
|
74
|
+
@ip.get
|
75
|
+
end
|
76
|
+
|
77
|
+
# Atomically update the last IP used by the user, and the last time they've
|
78
|
+
# been "seen" connecting to the app.
|
79
|
+
#
|
80
|
+
# @param ip [String, nil] the last-seen IP address for the user. If +nil+
|
81
|
+
# and we had a non-empty value before, we won't update it. Defaults to
|
82
|
+
# the current HTTP request's IP address, if any.
|
83
|
+
# @param seen_at [Time] the time at which we're making the update. We will
|
84
|
+
# always keep the most recent time if this conflicts with the current
|
85
|
+
# value.
|
86
|
+
# @return [void]
|
87
|
+
def update(seen_at: Time.now.utc, ip: Aikido::Zen.current_context&.request&.ip)
|
88
|
+
@last_seen_at.try_update { |last_seen_at| [last_seen_at, seen_at].max }
|
89
|
+
@ip.try_update { |last_ip| [ip, last_ip].compact.first }
|
90
|
+
end
|
91
|
+
|
92
|
+
# @return [self]
|
93
|
+
def to_aikido_actor
|
94
|
+
self
|
95
|
+
end
|
96
|
+
|
97
|
+
def ==(other)
|
98
|
+
other.is_a?(Actor) && id == other.id
|
99
|
+
end
|
100
|
+
alias_method :eql?, :==
|
101
|
+
|
102
|
+
def hash
|
103
|
+
id.hash
|
104
|
+
end
|
105
|
+
|
106
|
+
def as_json
|
107
|
+
{
|
108
|
+
id: id,
|
109
|
+
name: name,
|
110
|
+
lastIpAddress: ip,
|
111
|
+
firstSeenAt: first_seen_at.to_i * 1000,
|
112
|
+
lastSeenAt: last_seen_at.to_i * 1000
|
113
|
+
}.compact
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Aikido::Zen
|
4
|
+
# Handles scheduling the heartbeats we send to the Aikido servers, managing
|
5
|
+
# runtime changes to the heartbeat interval.
|
6
|
+
class Agent::HeartbeatsManager
|
7
|
+
def initialize(worker:, settings: Aikido::Zen.runtime_settings, config: Aikido::Zen.config)
|
8
|
+
@settings = settings
|
9
|
+
@config = config
|
10
|
+
@worker = worker
|
11
|
+
|
12
|
+
@timer = nil
|
13
|
+
end
|
14
|
+
|
15
|
+
# @return [Boolean]
|
16
|
+
def running?
|
17
|
+
!!@timer&.running?
|
18
|
+
end
|
19
|
+
|
20
|
+
# @return [Boolean] whether the currently running heartbeat matches the
|
21
|
+
# expected interval in the runtime settings.
|
22
|
+
def stale_settings?
|
23
|
+
running? && @timer.execution_interval != @settings.heartbeat_interval
|
24
|
+
end
|
25
|
+
|
26
|
+
# Sets up the the timer to run the given block at the appropriate interval.
|
27
|
+
# Re-entrant, and does nothing if already running.
|
28
|
+
#
|
29
|
+
# @return [void]
|
30
|
+
def start(&task)
|
31
|
+
return if running?
|
32
|
+
|
33
|
+
if @settings.heartbeat_interval&.nonzero?
|
34
|
+
@config.logger.debug "Scheduling heartbeats every #{@settings.heartbeat_interval} seconds"
|
35
|
+
@timer = @worker.every(@settings.heartbeat_interval, run_now: false, &task)
|
36
|
+
else
|
37
|
+
@config.logger.warn(format("Heartbeat could not be set up (interval: %p)", @settings.heartbeat_interval))
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Cleans up the timer.
|
42
|
+
#
|
43
|
+
# @return [void]
|
44
|
+
def stop
|
45
|
+
return unless running?
|
46
|
+
|
47
|
+
@timer.shutdown
|
48
|
+
@timer = nil
|
49
|
+
end
|
50
|
+
|
51
|
+
# Resets the timer to start with any new settings, if needed.
|
52
|
+
#
|
53
|
+
# @return [void]
|
54
|
+
def restart(&task)
|
55
|
+
stop
|
56
|
+
start(&task)
|
57
|
+
end
|
58
|
+
|
59
|
+
# @api private
|
60
|
+
#
|
61
|
+
# @return [Integer] the current delay between events.
|
62
|
+
def interval
|
63
|
+
@settings.heartbeat_interval
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,179 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "concurrent"
|
4
|
+
require_relative "event"
|
5
|
+
require_relative "config"
|
6
|
+
require_relative "system_info"
|
7
|
+
|
8
|
+
module Aikido::Zen
|
9
|
+
# Handles the background processes that communicate with the Aikido servers,
|
10
|
+
# including managing the runtime settings that keep the app protected.
|
11
|
+
class Agent
|
12
|
+
# Initialize and start an agent instance.
|
13
|
+
#
|
14
|
+
# @return [Aikido::Zen::Agent]
|
15
|
+
def self.start(**opts)
|
16
|
+
new(**opts).tap(&:start!)
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(
|
20
|
+
config: Aikido::Zen.config,
|
21
|
+
collector: Aikido::Zen.collector,
|
22
|
+
detached_agent: Aikido::Zen.detached_agent,
|
23
|
+
worker: Aikido::Zen::Worker.new(config: config),
|
24
|
+
api_client: Aikido::Zen::APIClient.new(config: config)
|
25
|
+
)
|
26
|
+
@started_at = nil
|
27
|
+
|
28
|
+
@config = config
|
29
|
+
@worker = worker
|
30
|
+
@api_client = api_client
|
31
|
+
@collector = collector
|
32
|
+
@detached_agent = detached_agent
|
33
|
+
end
|
34
|
+
|
35
|
+
def started?
|
36
|
+
!!@started_at
|
37
|
+
end
|
38
|
+
|
39
|
+
def start!
|
40
|
+
@config.logger.info "Starting Aikido agent v#{Aikido::Zen::VERSION}"
|
41
|
+
|
42
|
+
raise Aikido::ZenError, "Aikido Agent already started!" if started?
|
43
|
+
@started_at = Time.now.utc
|
44
|
+
@collector.start(at: @started_at)
|
45
|
+
|
46
|
+
if @config.blocking_mode?
|
47
|
+
@config.logger.info "Requests identified as attacks will be blocked"
|
48
|
+
else
|
49
|
+
@config.logger.warn "Non-blocking mode enabled! No requests will be blocked."
|
50
|
+
end
|
51
|
+
|
52
|
+
if @api_client.can_make_requests?
|
53
|
+
@config.logger.info "API Token set! Reporting has been enabled."
|
54
|
+
else
|
55
|
+
@config.logger.warn "No API Token set! Reporting has been disabled."
|
56
|
+
return
|
57
|
+
end
|
58
|
+
|
59
|
+
at_exit { stop! if started? }
|
60
|
+
|
61
|
+
report(Events::Started.new(time: @started_at)) do |response|
|
62
|
+
updated_settings! if Aikido::Zen.runtime_settings.update_from_json(response)
|
63
|
+
@config.logger.info "Updated runtime settings."
|
64
|
+
rescue => err
|
65
|
+
@config.logger.error(err.message)
|
66
|
+
end
|
67
|
+
|
68
|
+
poll_for_setting_updates
|
69
|
+
|
70
|
+
@worker.delay(@config.initial_heartbeat_delay) do
|
71
|
+
send_heartbeat if @collector.stats.any?
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Clean up any ongoing threads, and reset the state. Called automatically
|
76
|
+
# when the process exits.
|
77
|
+
#
|
78
|
+
# @return [void]
|
79
|
+
def stop!
|
80
|
+
@config.logger.info "Stopping Aikido agent"
|
81
|
+
@started_at = nil
|
82
|
+
@worker.shutdown
|
83
|
+
end
|
84
|
+
|
85
|
+
# Respond to the runtime settings changing after being fetched from the
|
86
|
+
# Aikido servers.
|
87
|
+
#
|
88
|
+
# @return [void]
|
89
|
+
def updated_settings!
|
90
|
+
if !heartbeats.running?
|
91
|
+
heartbeats.start { send_heartbeat }
|
92
|
+
elsif heartbeats.stale_settings?
|
93
|
+
heartbeats.restart { send_heartbeat }
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# Given an Attack, report it to the Aikido server, and/or block the request
|
98
|
+
# depending on configuration.
|
99
|
+
#
|
100
|
+
# @param attack [Attack] a detected attack.
|
101
|
+
# @return [void]
|
102
|
+
#
|
103
|
+
# @raise [Aikido::Zen::UnderAttackError] if the firewall is configured
|
104
|
+
# to block requests.
|
105
|
+
def handle_attack(attack)
|
106
|
+
attack.will_be_blocked! if @config.blocking_mode?
|
107
|
+
|
108
|
+
@config.logger.error(
|
109
|
+
format("Zen has %s a %s: %s", attack.blocked? ? "blocked" : "detected", attack.humanized_name, attack.as_json.to_json)
|
110
|
+
)
|
111
|
+
report(Events::Attack.new(attack: attack)) if @api_client.can_make_requests?
|
112
|
+
|
113
|
+
@collector.track_attack(attack)
|
114
|
+
raise attack if attack.blocked?
|
115
|
+
end
|
116
|
+
|
117
|
+
# Asynchronously reports an Event of any kind to the Aikido dashboard. If
|
118
|
+
# given a block, the API response will be passed to the block for handling.
|
119
|
+
#
|
120
|
+
# @param event [Aikido::Zen::Event]
|
121
|
+
# @yieldparam response [Object] the response from the reporting API in case
|
122
|
+
# of a successful request.
|
123
|
+
#
|
124
|
+
# @return [void]
|
125
|
+
def report(event)
|
126
|
+
@worker.perform do
|
127
|
+
response = @api_client.report(event)
|
128
|
+
yield response if response && block_given?
|
129
|
+
rescue Aikido::Zen::APIError, Aikido::Zen::NetworkError => err
|
130
|
+
@config.logger.error(err.message)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# @api private
|
135
|
+
#
|
136
|
+
# Atomically flushes all the stats stored by the agent, and sends a
|
137
|
+
# heartbeat event. Scheduled to run automatically on a recurring schedule
|
138
|
+
# when reporting is enabled.
|
139
|
+
#
|
140
|
+
# @param at [Time] the event time. Defaults to now.
|
141
|
+
# @return [void]
|
142
|
+
# @see Aikido::Zen::RuntimeSettings#heartbeat_interval
|
143
|
+
def send_heartbeat(at: Time.now.utc)
|
144
|
+
return unless @api_client.can_make_requests?
|
145
|
+
|
146
|
+
@collector.flush_heartbeats.each do |heartbeat|
|
147
|
+
report(heartbeat) do |response|
|
148
|
+
updated_settings! if Aikido::Zen.runtime_settings.update_from_json(response)
|
149
|
+
@config.logger.info "Updated runtime settings after heartbeat"
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
# @api private
|
155
|
+
#
|
156
|
+
# Sets up the timer task that polls the Aikido Runtime API for updates to
|
157
|
+
# the runtime settings every minute.
|
158
|
+
#
|
159
|
+
# @return [void]
|
160
|
+
# @see Aikido::Zen::RuntimeSettings
|
161
|
+
def poll_for_setting_updates
|
162
|
+
@worker.every(@config.polling_interval) do
|
163
|
+
if @api_client.should_fetch_settings?
|
164
|
+
updated_settings! if Aikido::Zen.runtime_settings.update_from_json(@api_client.fetch_settings)
|
165
|
+
@config.logger.info "Updated runtime settings after polling"
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
private def heartbeats
|
171
|
+
@heartbeats ||= Aikido::Zen::Agent::HeartbeatsManager.new(
|
172
|
+
config: @config,
|
173
|
+
worker: @worker
|
174
|
+
)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
require_relative "agent/heartbeats_manager"
|
@@ -0,0 +1,145 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "net/http"
|
4
|
+
require_relative "rate_limiter"
|
5
|
+
|
6
|
+
module Aikido::Zen
|
7
|
+
# Implements all communication with the Aikido servers.
|
8
|
+
class APIClient
|
9
|
+
def initialize(
|
10
|
+
config: Aikido::Zen.config,
|
11
|
+
rate_limiter: Aikido::Zen::RateLimiter::Breaker.new,
|
12
|
+
system_info: Aikido::Zen.system_info
|
13
|
+
)
|
14
|
+
@config = config
|
15
|
+
@system_info = system_info
|
16
|
+
@rate_limiter = rate_limiter
|
17
|
+
end
|
18
|
+
|
19
|
+
# @return [Boolean] whether we have a configured token.
|
20
|
+
def can_make_requests?
|
21
|
+
@config.api_token.to_s.size > 0
|
22
|
+
end
|
23
|
+
|
24
|
+
# Checks with the Aikido Runtime API the timestamp of the last settings
|
25
|
+
# update, and compares against the given value.
|
26
|
+
#
|
27
|
+
# @param last_updated_at [Time]
|
28
|
+
#
|
29
|
+
# @return [Boolean]
|
30
|
+
# @raise (see #request)
|
31
|
+
def should_fetch_settings?(last_updated_at = Aikido::Zen.runtime_settings.updated_at)
|
32
|
+
@config.logger.debug("Polling for new runtime settings to fetch")
|
33
|
+
|
34
|
+
return false unless can_make_requests?
|
35
|
+
return true if last_updated_at.nil?
|
36
|
+
|
37
|
+
response = request(
|
38
|
+
Net::HTTP::Get.new("/config", default_headers),
|
39
|
+
base_url: @config.realtime_endpoint
|
40
|
+
)
|
41
|
+
|
42
|
+
new_updated_at = Time.at(response["configUpdatedAt"].to_i / 1000)
|
43
|
+
new_updated_at > last_updated_at
|
44
|
+
end
|
45
|
+
|
46
|
+
# Fetches the runtime settings from the server. In case of a timeout or
|
47
|
+
# other low-lever error, the request will be automatically retried up to two
|
48
|
+
# times, after which it will raise an error.
|
49
|
+
#
|
50
|
+
# @return [Hash] decoded JSON response from the server with the runtime
|
51
|
+
# settings.
|
52
|
+
# @raise (see #request)
|
53
|
+
def fetch_settings
|
54
|
+
@config.logger.debug("Fetching new runtime settings")
|
55
|
+
|
56
|
+
request(Net::HTTP::Get.new("/api/runtime/config", default_headers))
|
57
|
+
end
|
58
|
+
|
59
|
+
# @overload report(event)
|
60
|
+
# Reports an event to the server.
|
61
|
+
#
|
62
|
+
# @param event [Aikido::Zen::Event]
|
63
|
+
# @return [void]
|
64
|
+
# @raise (see #request)
|
65
|
+
#
|
66
|
+
# @overload report(settings_updating_event)
|
67
|
+
# Reports an event that responds with updated runtime settings, and
|
68
|
+
# requires us to update settings afterwards.
|
69
|
+
#
|
70
|
+
# @param settings_updating_event [Aikido::Zen::Events::Started,
|
71
|
+
# Aikido::Zen::Events::Heartbeat]
|
72
|
+
# @return (see #fetch_settings)
|
73
|
+
# @raise (see #request)
|
74
|
+
def report(event)
|
75
|
+
event_type = if event.respond_to?(:type)
|
76
|
+
event.type
|
77
|
+
else
|
78
|
+
event[:type]
|
79
|
+
end
|
80
|
+
|
81
|
+
if @rate_limiter.throttle?(event_type)
|
82
|
+
@config.logger.error("Not reporting #{event_type.upcase} event due to rate limiting")
|
83
|
+
return
|
84
|
+
end
|
85
|
+
|
86
|
+
@config.logger.debug("Reporting #{event_type.upcase} event")
|
87
|
+
|
88
|
+
req = Net::HTTP::Post.new("/api/runtime/events", default_headers)
|
89
|
+
req.content_type = "application/json"
|
90
|
+
req.body = if event.respond_to?(:as_json)
|
91
|
+
@config.json_encoder.call(event.as_json)
|
92
|
+
else
|
93
|
+
@config.json_encoder.call(event)
|
94
|
+
end
|
95
|
+
|
96
|
+
request(req)
|
97
|
+
rescue Aikido::Zen::RateLimitedError
|
98
|
+
@rate_limiter.open!
|
99
|
+
raise
|
100
|
+
end
|
101
|
+
|
102
|
+
# Perform an HTTP request against one of our API endpoints, and process the
|
103
|
+
# response.
|
104
|
+
#
|
105
|
+
# @param request [Net::HTTPRequest]
|
106
|
+
# @param base_url [URI] which API to use. Defaults to +Config#api_endpoint+.
|
107
|
+
#
|
108
|
+
# @return [Object] the result of decoding the JSON response from the server.
|
109
|
+
#
|
110
|
+
# @raise [Aikido::Zen::APIError] in case of a 4XX or 5XX response.
|
111
|
+
# @raise [Aikido::Zen::NetworkError] if an error occurs trying to make the
|
112
|
+
# request.
|
113
|
+
private def request(request, base_url: @config.api_endpoint)
|
114
|
+
Net::HTTP.start(base_url.host, base_url.port, http_settings(base_url)) do |http|
|
115
|
+
response = http.request(request)
|
116
|
+
|
117
|
+
case response
|
118
|
+
when Net::HTTPSuccess
|
119
|
+
@config.json_decoder.call(response.body)
|
120
|
+
when Net::HTTPTooManyRequests
|
121
|
+
raise RateLimitedError.new(request, response)
|
122
|
+
else
|
123
|
+
raise APIError.new(request, response)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
rescue Timeout::Error, IOError, SystemCallError, OpenSSL::OpenSSLError => err
|
127
|
+
raise NetworkError.new(request, err)
|
128
|
+
end
|
129
|
+
|
130
|
+
private def http_settings(base_url)
|
131
|
+
@http_settings ||= {
|
132
|
+
use_ssl: base_url.scheme == "https",
|
133
|
+
max_retries: 2
|
134
|
+
}.merge(@config.api_timeouts)
|
135
|
+
end
|
136
|
+
|
137
|
+
private def default_headers
|
138
|
+
@default_headers ||= {
|
139
|
+
"Authorization" => @config.api_token,
|
140
|
+
"Accept" => "application/json",
|
141
|
+
"User-Agent" => "#{@system_info.library_name} v#{@system_info.library_version}"
|
142
|
+
}
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|