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
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