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
data/README.md ADDED
@@ -0,0 +1,40 @@
1
+ # Aikido::Firewall
2
+
3
+ TODO: Write me :)
4
+
5
+ ## Installation
6
+
7
+ Install the gem and add to the application's Gemfile by executing:
8
+
9
+ $ bundle add aikido-firewall
10
+
11
+ If bundler is not being used to manage dependencies, install the gem by executing:
12
+
13
+ $ gem install aikido-firewall
14
+
15
+ ## Development
16
+
17
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run
18
+ `rake test` to run the tests. You can also run `bin/console` for an interactive
19
+ prompt that will allow you to experiment.
20
+
21
+ To install this gem onto your local machine, run `bundle exec rake install`. To
22
+ release a new version, update the version number in `version.rb`, and then run
23
+ `bundle exec rake release`, which will create a git tag for the version, push
24
+ git commits and the created tag, and push the `.gem` file to
25
+ [rubygems.org](https://rubygems.org).
26
+
27
+ ## Contributing
28
+
29
+ Bug reports and pull requests are welcome [on GitHub][repo]. This project is
30
+ intended to be a safe, welcoming space for collaboration, and contributors are
31
+ expected to adhere to the [code of conduct][coc].
32
+
33
+ ## Code of Conduct
34
+
35
+ Everyone interacting in the `Aikido::Firewall` project's codebases, issue
36
+ trackers, chat rooms and mailing lists is expected to follow the [code of
37
+ conduct][coc].
38
+
39
+ [repo]: https://github.com/aikidosec/firewall-ruby
40
+ [coc]: https://github.com/aikidosec/firewall-ruby/blob/main/CODE_OF_CONDUCT.md
data/Rakefile ADDED
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+ require "standard/rake"
6
+ require "rake/clean"
7
+
8
+ load "tasklib/libzen.rake"
9
+
10
+ namespace :build do
11
+ desc "Ensure Gemfile.lock is up-to-date"
12
+ task "update_gem_lockfile" do
13
+ sh "bundle check >/dev/null || bundle"
14
+ end
15
+ end
16
+ task build: ["build:update_gem_lockfile", "libzen:download:all"]
17
+
18
+ # Build all the native gems as well
19
+ Rake::Task["build"].enhance(["libzen:gems"])
20
+
21
+ # rake release wants to tag the commit and push the tag, but we run the release
22
+ # workflow after creating the tag, and so we don't need another one.
23
+ Rake::Task["release:source_control_push"].clear
24
+ task "release:source_control_push" do
25
+ # do nothing
26
+ end
27
+
28
+ # Push all the native gems before the libzen-less one.
29
+ task "release:rubygem_push" => "libzen:release"
30
+
31
+ Pathname.glob("sample_apps/*").select(&:directory?).each do |dir|
32
+ namespace :build do
33
+ desc "Ensure Gemfile.lock is up-to-date in the #{dir.basename} sample app"
34
+ task "update_#{dir.basename}_lockfile" do
35
+ Dir.chdir(dir) { sh "bundle check >/dev/null || bundle" }
36
+ end
37
+ end
38
+
39
+ task build: "build:update_#{dir.basename}_lockfile"
40
+ end
41
+
42
+ Minitest::TestTask.create do |test_task|
43
+ test_task.test_globs = FileList["test/**/{test_*,*_test}.rb"]
44
+ .exclude("test/e2e/**/*.rb")
45
+ end
46
+ task test: "libzen:download:current"
47
+
48
+ Pathname.glob("test/e2e/*").select(&:directory?).each do |dir|
49
+ namespace :e2e do
50
+ desc "Run e2e tests for the #{dir.basename} sample app"
51
+ task dir.basename do
52
+ Dir.chdir(dir) do
53
+ sh "rake ci:setup"
54
+ sh "rake test"
55
+ end
56
+ end
57
+ end
58
+
59
+ desc "Run all e2e tests"
60
+ task e2e: "e2e:#{dir.basename}"
61
+ end
62
+
63
+ task default: %i[test standard]
@@ -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
+ }
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,187 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent"
4
+ require_relative "event"
5
+ require_relative "stats"
6
+ require_relative "config"
7
+ require_relative "system_info"
8
+
9
+ module Aikido::Zen
10
+ # Handles the background processes that communicate with the Aikido servers,
11
+ # including managing the runtime settings that keep the app protected.
12
+ class Agent
13
+ # @return [Aikido::Zen::Stats] the statistics collected by the agent.
14
+ attr_reader :stats
15
+
16
+ def initialize(
17
+ stats: Aikido::Zen::Stats.new,
18
+ config: Aikido::Zen.config,
19
+ info: Aikido::Zen.system_info,
20
+ api_client: Aikido::Zen::APIClient.new
21
+ )
22
+ @started_at = nil
23
+
24
+ @stats = stats
25
+ @info = info
26
+ @config = config
27
+ @api_client = api_client
28
+ @timer_tasks = []
29
+ @delayed_tasks = []
30
+ @reporting_pool = nil
31
+ @heartbeats = nil
32
+ end
33
+
34
+ def started?
35
+ !!@started_at
36
+ end
37
+
38
+ # Given an Attack, report it to the Aikido server, and/or block the request
39
+ # depending on configuration.
40
+ #
41
+ # @param attack [Attack] a detected attack.
42
+ # @return [void]
43
+ #
44
+ # @raise [Aikido::Zen::UnderAttackError] if the firewall is configured
45
+ # to block requests.
46
+ def handle_attack(attack)
47
+ attack.will_be_blocked! if @config.blocking_mode?
48
+
49
+ @config.logger.error("[ATTACK DETECTED] #{attack.log_message}")
50
+ report(Events::Attack.new(attack: attack)) if @api_client.can_make_requests?
51
+
52
+ stats.add_attack(attack, being_blocked: @config.blocking_mode?)
53
+ raise attack if @config.blocking_mode?
54
+ end
55
+
56
+ # Asynchronously reports an Event of any kind to the Aikido dashboard. If
57
+ # given a block, the API response will be passed to the block for handling.
58
+ #
59
+ # @param event [Aikido::Zen::Event]
60
+ # @yieldparam response [Object] the response from the reporting API in case
61
+ # of a successful request.
62
+ #
63
+ # @return [void]
64
+ def report(event)
65
+ reporting_pool.post do
66
+ response = @api_client.report(event)
67
+ yield response if response && block_given?
68
+ rescue Aikido::Zen::APIError, Aikido::Zen::NetworkError => err
69
+ @config.logger.error(err.message)
70
+ end
71
+ end
72
+
73
+ def start!
74
+ @config.logger.info "Starting Aikido agent"
75
+
76
+ raise Aikido::ZenError, "Aikido Agent already started!" if started?
77
+ @started_at = Time.now.utc
78
+
79
+ stats.start(@started_at)
80
+
81
+ if @config.blocking_mode?
82
+ @config.logger.info "Requests identified as attacks will be blocked"
83
+ else
84
+ @config.logger.warn "Non-blocking mode enabled! No requests will be blocked."
85
+ end
86
+
87
+ if @api_client.can_make_requests?
88
+ @config.logger.info "API Token set! Reporting has been enabled."
89
+ else
90
+ @config.logger.warn "No API Token set! Reporting has been disabled."
91
+ return
92
+ end
93
+
94
+ # Subscribe to firewall setting changes so we can correctly re-configure
95
+ # the heartbeat process.
96
+ Aikido::Zen.runtime_settings.add_observer(self, :setup_heartbeat)
97
+
98
+ at_exit { stop! if started? }
99
+
100
+ report(Events::Started.new(time: @started_at)) do |response|
101
+ Aikido::Zen.runtime_settings.update_from_json(response)
102
+ @config.logger.info "Updated runtime settings."
103
+ end
104
+
105
+ poll_for_setting_updates
106
+ end
107
+
108
+ def send_heartbeat
109
+ return unless @api_client.can_make_requests?
110
+
111
+ flushed_stats = stats.reset
112
+ report(Events::Heartbeat.new(stats: flushed_stats)) do |response|
113
+ Aikido::Zen.runtime_settings.update_from_json(response)
114
+ @config.logger.info "Updated runtime settings after heartbeat"
115
+ end
116
+ end
117
+
118
+ def poll_for_setting_updates
119
+ timer_task(every: @config.polling_interval) do
120
+ if @api_client.should_fetch_settings?
121
+ Aikido::Zen.runtime_settings.update_from_json(@api_client.fetch_settings)
122
+ @config.logger.info "Updated runtime settings after polling"
123
+ end
124
+ end
125
+ end
126
+
127
+ def setup_heartbeat(settings)
128
+ return unless @api_client.can_make_requests?
129
+
130
+ # If the desired interval changed, then clear the current heartbeat timer
131
+ # and set up a new one.
132
+ if @heartbeats&.running? && @heartbeats.execution_interval != settings.heartbeat_interval
133
+ @heartbeats.shutdown
134
+ @heartbeats = nil
135
+ setup_heartbeat(settings)
136
+
137
+ # If the heartbeat timer isn't running but we know how often it should run, schedule it.
138
+ elsif !@heartbeats&.running? && settings.heartbeat_interval&.nonzero?
139
+ @config.logger.debug "Scheduling heartbeats every #{settings.heartbeat_interval} seconds"
140
+ @heartbeats = timer_task(every: settings.heartbeat_interval, run_now: false) do
141
+ send_heartbeat
142
+ end
143
+
144
+ elsif !@heartbeats&.running?
145
+ @config.logger.debug(format("Heartbeat could not be setup (interval: %p)", settings.heartbeat_interval))
146
+ end
147
+
148
+ # If the server hasn't received any stats, we want to also run a one-off
149
+ # heartbeat request in a minute.
150
+ if !settings.received_any_stats
151
+ delay(@config.initial_heartbeat_delay) { send_heartbeat if stats.any? }
152
+ end
153
+ end
154
+
155
+ def stop!
156
+ @started_at = nil
157
+
158
+ @config.logger.info "Stopping Aikido agent"
159
+
160
+ @timer_tasks.each { |task| task.shutdown }
161
+ @delayed_tasks.each { |task| task.cancel if task.pending? }
162
+
163
+ @reporting_pool&.shutdown
164
+ @reporting_pool&.wait_for_termination(30)
165
+ end
166
+
167
+ private def reporting_pool
168
+ @reporting_pool ||= Concurrent::SingleThreadExecutor.new
169
+ end
170
+
171
+ private def delay(delay, &task)
172
+ Concurrent::ScheduledTask.execute(delay, executor: reporting_pool, &task)
173
+ .tap { |task| @delayed_tasks << task }
174
+ end
175
+
176
+ private def timer_task(every:, **opts, &block)
177
+ Concurrent::TimerTask.execute(
178
+ run_now: true,
179
+ interval_type: :fixed_rate,
180
+ execution_interval: every,
181
+ executor: reporting_pool,
182
+ **opts,
183
+ &block
184
+ ).tap { |task| @timer_tasks << task }
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,132 @@
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.runtime_api_base_url
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
+ if @rate_limiter.throttle?(event)
76
+ @config.logger.error("Not reporting #{event.type.upcase} event due to rate limiting")
77
+ return
78
+ end
79
+
80
+ @config.logger.debug("Reporting #{event.type.upcase} event")
81
+
82
+ req = Net::HTTP::Post.new("/api/runtime/events", default_headers)
83
+ req.content_type = "application/json"
84
+ req.body = @config.json_encoder.call(event.as_json)
85
+
86
+ request(req)
87
+ rescue Aikido::Zen::RateLimitedError
88
+ @rate_limiter.open!
89
+ raise
90
+ end
91
+
92
+ # Perform an HTTP request against one of our API endpoints, and process the
93
+ # response.
94
+ #
95
+ # @param request [Net::HTTPRequest]
96
+ # @param base_url [URI] which API to use. Defaults to +Config#api_base_url+.
97
+ #
98
+ # @return [Object] the result of decoding the JSON response from the server.
99
+ #
100
+ # @raise [Aikido::Zen::APIError] in case of a 4XX or 5XX response.
101
+ # @raise [Aikido::Zen::NetworkError] if an error occurs trying to make the
102
+ # request.
103
+ private def request(request, base_url: @config.api_base_url)
104
+ Net::HTTP.start(base_url.host, base_url.port, http_settings) do |http|
105
+ response = http.request(request)
106
+
107
+ case response
108
+ when Net::HTTPSuccess
109
+ @config.json_decoder.call(response.body)
110
+ when Net::HTTPTooManyRequests
111
+ raise RateLimitedError.new(request, response)
112
+ else
113
+ raise APIError.new(request, response)
114
+ end
115
+ end
116
+ rescue Timeout::Error, IOError, SystemCallError, OpenSSL::OpenSSLError => err
117
+ raise NetworkError.new(request, err)
118
+ end
119
+
120
+ private def http_settings
121
+ @http_settings ||= {use_ssl: true, max_retries: 2}.merge(@config.api_timeouts)
122
+ end
123
+
124
+ private def default_headers
125
+ @default_headers ||= {
126
+ "Authorization" => @config.api_token,
127
+ "Accept" => "application/json",
128
+ "User-Agent" => "#{@system_info.library_name} v#{@system_info.library_version}"
129
+ }
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikido::Zen
4
+ # Attack objects gather information about a type of detected attack.
5
+ # They can be used in a few ways, like for reporting an attack event
6
+ # to the Aikido server, or can be raised as errors to block requests
7
+ # if blocking_mode is on.
8
+ class Attack
9
+ attr_reader :context
10
+ attr_reader :operation
11
+ attr_accessor :sink
12
+
13
+ def initialize(context:, sink:, operation:)
14
+ @context = context
15
+ @operation = operation
16
+ @sink = sink
17
+ @blocked = false
18
+ end
19
+
20
+ def will_be_blocked!
21
+ @blocked = true
22
+ end
23
+
24
+ def blocked?
25
+ @blocked
26
+ end
27
+
28
+ def log_message
29
+ raise NotImplementedError, "implement in subclasses"
30
+ end
31
+
32
+ def as_json
33
+ raise NotImplementedError, "implement in subclasses"
34
+ end
35
+
36
+ def exception(*)
37
+ raise NotImplementedError, "implement in subclasses"
38
+ end
39
+ end
40
+
41
+ module Attacks
42
+ class SQLInjectionAttack < Attack
43
+ attr_reader :query
44
+ attr_reader :input
45
+ attr_reader :dialect
46
+
47
+ def initialize(query:, input:, dialect:, **opts)
48
+ super(**opts)
49
+ @query = query
50
+ @input = input
51
+ @dialect = dialect
52
+ end
53
+
54
+ def log_message
55
+ format(
56
+ "SQL Injection: Malicious user input «%s» detected in %s query «%s»",
57
+ @input, @dialect, @query
58
+ )
59
+ end
60
+
61
+ def as_json
62
+ {
63
+ kind: "sql_injection",
64
+ blocked: blocked?,
65
+ metadata: {sql: @query},
66
+ operation: @operation
67
+ }.merge(@input.as_json)
68
+ end
69
+
70
+ def exception(*)
71
+ SQLInjectionError.new(self)
72
+ end
73
+ end
74
+
75
+ class SSRFAttack < Attack
76
+ attr_reader :input
77
+ attr_reader :request
78
+
79
+ def initialize(request:, input:, **opts)
80
+ super(**opts)
81
+ @input = input
82
+ @request = request
83
+ end
84
+
85
+ def log_message
86
+ format(
87
+ "SSRF: Request to user-supplied hostname «%s» detected in %s (%s).",
88
+ @input, @operation, @request
89
+ ).strip
90
+ end
91
+
92
+ def exception(*)
93
+ SSRFDetectedError.new(self)
94
+ end
95
+
96
+ def as_json
97
+ {
98
+ kind: "ssrf",
99
+ metadata: {host: @request.uri.hostname, port: @request.uri.port},
100
+ blocked: blocked?,
101
+ operation: @operation
102
+ }.merge(@input.as_json)
103
+ end
104
+ end
105
+
106
+ # Special case of an SSRF attack where we don't have a context—we're just
107
+ # detecting a request to a particularly sensitive address.
108
+ class StoredSSRFAttack < Attack
109
+ attr_reader :hostname
110
+ attr_reader :address
111
+
112
+ def initialize(hostname:, address:, **opts)
113
+ super(**opts)
114
+ @hostname = hostname
115
+ @address = address
116
+ end
117
+
118
+ def log_message
119
+ format(
120
+ "Stored SSRF: Request to sensitive host «%s» (%s) detected from unknown source in %s",
121
+ @hostname, @address, @operation
122
+ )
123
+ end
124
+
125
+ def exception(*)
126
+ SSRFDetectedError.new(self)
127
+ end
128
+
129
+ def as_json
130
+ {
131
+ kind: "ssrf",
132
+ blocked: blocked?,
133
+ operation: @operation
134
+ }
135
+ end
136
+ end
137
+ end
138
+ end