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
data/docs/rails.md ADDED
@@ -0,0 +1,112 @@
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.option = value
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 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
+ You can override the logger to integrate with your application logging strategy:
106
+
107
+ ```ruby
108
+ # config/initializers/zen.rb
109
+ Rails.application.config.zen.logger = ::Rails.logger
110
+ ```
111
+
112
+ Zen expects an instance of Ruby's [Logger](https://github.com/ruby/logger) class.
@@ -0,0 +1,62 @@
1
+ # Troubleshooting
2
+
3
+ If the Zen Firewall isn't working as expected, follow these steps in order to diagnose common issues.
4
+
5
+ ## Review installation steps
6
+
7
+ Double-check your setup against the [installation guide](../README.md#installation).
8
+
9
+ Make sure:
10
+ - Your runtime and framework are supported (see [Supported libraries and frameworks](../README.md#supported-libraries-and-frameworks)).
11
+ - The package installed successfully.
12
+ - Your framework-specific integration matches the example in the docs (for Rails, see the example in [docs/rails.md](../docs/rails.md)).
13
+
14
+ ## Check connection to Aikido
15
+
16
+ The firewall must be able to reach Aikido's API endpoints.
17
+
18
+ Test connectivity from the same environment where your app runs, following the instructions on this page: https://help.aikido.dev/zen-firewall/miscellaneous/outbound-network-connections-for-zen
19
+
20
+ ## Check logs for errors
21
+
22
+ Common places:
23
+ - Local dev: `cat log/development.log` or `tail -f log/development.log`
24
+ - Docker: `docker logs <your-app-container>`
25
+ - systemd: `journalctl -u <your-app-service> --since "1 hour ago"`
26
+
27
+ Tip: search logs for lines containing `Aikido` or `Zen`.
28
+
29
+ For example:
30
+
31
+ ```sh
32
+ grep -Ei 'aikido|zen' log/development.log
33
+ ```
34
+
35
+ ## Enable debug output temporarily
36
+
37
+ If you use Rails, set the log level to `info` or `debug` while you investigate.
38
+
39
+ ```ruby
40
+ # config/environments/development.rb or production.rb
41
+ config.log_level = :info # use :debug if needed
42
+ ```
43
+
44
+ If you have your own logger, make sure Zen logs go to stdout.
45
+
46
+ You can enable Zen debugging mode as follows.
47
+
48
+ ```ruby
49
+ # config/initializers/zen.rb
50
+ Rails.application.config.zen.debugging = true
51
+ ```
52
+
53
+ Or set `AIKIDO_DEBUG=true` in your environment.
54
+
55
+ ## Contact support
56
+
57
+ If you still can't resolve the problem:
58
+
59
+ - Use the in-app chat to reach our support team directly.
60
+ - Or create an issue on [GitHub](https://github.com/AikidoSec/firewall-ruby/issues) with details about your setup, framework, and logs.
61
+
62
+ Include as much context as possible; this helps us respond faster.
@@ -0,0 +1,146 @@
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
+ def self.from_json(data)
42
+ new(
43
+ id: data[:id],
44
+ name: data[:name],
45
+ ip: data[:lastIpAddress],
46
+ first_seen_at: Time.at(data[:firstSeenAt] / 1000),
47
+ last_seen_at: Time.at(data[:lastSeenAt] / 1000)
48
+ )
49
+ end
50
+
51
+ # @return [String] a unique identifier for this user.
52
+ attr_reader :id
53
+
54
+ # @return [String, nil] an optional name to display in the Aikido UI.
55
+ attr_reader :name
56
+
57
+ # @return [Time]
58
+ attr_reader :first_seen_at
59
+
60
+ # @param id [String]
61
+ # @param name [String, nil]
62
+ # @param ip [String, nil]
63
+ # @param first_seen_at [Time]
64
+ # @param last_seen_at [Time]
65
+ def initialize(
66
+ id:,
67
+ name: nil,
68
+ ip: Aikido::Zen.current_context&.request&.ip,
69
+ first_seen_at: Time.now.utc,
70
+ last_seen_at: first_seen_at
71
+ )
72
+ @id = id
73
+ @name = name
74
+ @ip = Concurrent::AtomicReference.new(ip)
75
+ @first_seen_at = first_seen_at
76
+ @last_seen_at = Concurrent::AtomicReference.new(last_seen_at)
77
+ end
78
+
79
+ # @return [Time]
80
+ def last_seen_at
81
+ @last_seen_at.get
82
+ end
83
+
84
+ # @return [String, nil] the IP address last used by this user, if available.
85
+ def ip
86
+ @ip.get
87
+ end
88
+
89
+ # Atomically update the last IP used by the user, and the last time they've
90
+ # been "seen" connecting to the app.
91
+ #
92
+ # @param ip [String, nil] the last-seen IP address for the user. If +nil+
93
+ # and we had a non-empty value before, we won't update it. Defaults to
94
+ # the current HTTP request's IP address, if any.
95
+ # @param seen_at [Time] the time at which we're making the update. We will
96
+ # always keep the most recent time if this conflicts with the current
97
+ # value.
98
+ # @return [void]
99
+ def update(seen_at: Time.now.utc, ip: Aikido::Zen.current_context&.request&.ip)
100
+ @last_seen_at.try_update { |last_seen_at| [last_seen_at, seen_at].max }
101
+ @ip.try_update { |last_ip| [ip, last_ip].compact.first }
102
+ end
103
+
104
+ # Merges the actor with another actor.
105
+ #
106
+ # @param other [Aikido::Zen::Actor]
107
+ # @return [Aikido::Zen::Actor]
108
+ def merge(other)
109
+ older = (first_seen_at < other.first_seen_at) ? self : other
110
+ newer = (last_seen_at > other.last_seen_at) ? self : other
111
+
112
+ self.class.new(
113
+ id: @id,
114
+ name: newer.name,
115
+ ip: newer.ip,
116
+ first_seen_at: older.first_seen_at,
117
+ last_seen_at: newer.last_seen_at
118
+ )
119
+ end
120
+ alias_method :|, :merge
121
+
122
+ # @return [self]
123
+ def to_aikido_actor
124
+ self
125
+ end
126
+
127
+ def ==(other)
128
+ other.is_a?(Actor) && id == other.id
129
+ end
130
+ alias_method :eql?, :==
131
+
132
+ def hash
133
+ id.hash
134
+ end
135
+
136
+ def as_json
137
+ {
138
+ id: id,
139
+ name: name,
140
+ lastIpAddress: ip,
141
+ firstSeenAt: first_seen_at.to_i * 1000,
142
+ lastSeenAt: last_seen_at.to_i * 1000
143
+ }.compact
144
+ end
145
+ end
146
+ 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 != 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 interval&.nonzero?
34
+ @config.logger.debug "Scheduling heartbeats every #{interval} seconds"
35
+ @timer = @worker.every(interval, run_now: false, &task)
36
+ else
37
+ @config.logger.warn(format("Heartbeat could not be set up (interval: %p)", 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,181 @@
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 Aikido::Zen.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
+ @config.initial_heartbeat_delays.each do |heartbeat_delay|
71
+ @worker.delay(heartbeat_delay) do
72
+ send_heartbeat
73
+ @config.logger.info("Executed initial heartbeat after #{heartbeat_delay} seconds")
74
+ end
75
+ end
76
+ end
77
+
78
+ # Clean up any ongoing threads, and reset the state. Called automatically
79
+ # when the process exits.
80
+ #
81
+ # @return [void]
82
+ def stop!
83
+ @config.logger.info("Stopping Aikido agent")
84
+ @started_at = nil
85
+ @worker.shutdown
86
+ end
87
+
88
+ # Respond to the runtime settings changing after being fetched from the
89
+ # Aikido servers.
90
+ #
91
+ # @return [void]
92
+ def updated_settings!
93
+ if !heartbeats.running?
94
+ heartbeats.start { send_heartbeat }
95
+ elsif heartbeats.stale_settings?
96
+ heartbeats.restart { send_heartbeat }
97
+ end
98
+ end
99
+
100
+ # Given an Attack, report it to the Aikido server, and/or block the request
101
+ # depending on configuration.
102
+ #
103
+ # @param attack [Attack] a detected attack.
104
+ # @return [void]
105
+ #
106
+ # @raise [Aikido::Zen::UnderAttackError] if the firewall is configured
107
+ # to block requests.
108
+ def handle_attack(attack)
109
+ attack.will_be_blocked! if Aikido::Zen.blocking_mode?
110
+
111
+ @config.logger.error(
112
+ format("Zen has %s a %s: %s", attack.blocked? ? "blocked" : "detected", attack.humanized_name, attack.as_json.to_json)
113
+ )
114
+ report(Events::Attack.new(attack: attack)) if @api_client.can_make_requests?
115
+
116
+ @collector.track_attack(attack)
117
+ raise attack if attack.blocked?
118
+ end
119
+
120
+ # Asynchronously reports an Event of any kind to the Aikido dashboard. If
121
+ # given a block, the API response will be passed to the block for handling.
122
+ #
123
+ # @param event [Aikido::Zen::Event]
124
+ # @yieldparam response [Object] the response from the reporting API in case
125
+ # of a successful request.
126
+ #
127
+ # @return [void]
128
+ def report(event)
129
+ @worker.perform do
130
+ response = @api_client.report(event)
131
+ yield response if response && block_given?
132
+ rescue Aikido::Zen::APIError, Aikido::Zen::NetworkError => err
133
+ @config.logger.error(err.message)
134
+ end
135
+ end
136
+
137
+ # @api private
138
+ #
139
+ # Atomically flushes all the stats stored by the agent, and sends a
140
+ # heartbeat event. Scheduled to run automatically on a recurring schedule
141
+ # when reporting is enabled.
142
+ #
143
+ # @param at [Time] the event time. Defaults to now.
144
+ # @return [void]
145
+ # @see Aikido::Zen::RuntimeSettings#heartbeat_interval
146
+ def send_heartbeat(at: Time.now.utc)
147
+ return unless @api_client.can_make_requests?
148
+
149
+ heartbeat = @collector.flush
150
+ report(heartbeat) do |response|
151
+ updated_settings! if Aikido::Zen.runtime_settings.update_from_json(response)
152
+ @config.logger.info("Updated runtime settings after heartbeat")
153
+ end
154
+ end
155
+
156
+ # @api private
157
+ #
158
+ # Sets up the timer task that polls the Aikido Runtime API for updates to
159
+ # the runtime settings every minute.
160
+ #
161
+ # @return [void]
162
+ # @see Aikido::Zen::RuntimeSettings
163
+ def poll_for_setting_updates
164
+ @worker.every(@config.polling_interval) do
165
+ if @api_client.should_fetch_settings?
166
+ updated_settings! if Aikido::Zen.runtime_settings.update_from_json(@api_client.fetch_settings)
167
+ @config.logger.info("Updated runtime settings after polling")
168
+ end
169
+ end
170
+ end
171
+
172
+ private def heartbeats
173
+ @heartbeats ||= Aikido::Zen::Agent::HeartbeatsManager.new(
174
+ config: @config,
175
+ worker: @worker
176
+ )
177
+ end
178
+ end
179
+ end
180
+
181
+ 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