aikido-zen 1.0.2.beta.5-arm64-linux-musl → 1.0.2.beta.6-arm64-linux-musl

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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/.simplecov +6 -0
  3. data/README.md +1 -0
  4. data/benchmarks/README.md +0 -1
  5. data/benchmarks/rails7.1_benchmark.js +1 -0
  6. data/benchmarks/rails7.1_sql_injection.js +52 -20
  7. data/docs/rails.md +5 -7
  8. data/lib/aikido/zen/actor.rb +34 -4
  9. data/lib/aikido/zen/agent/heartbeats_manager.rb +5 -5
  10. data/lib/aikido/zen/agent.rb +17 -15
  11. data/lib/aikido/zen/collector/event.rb +209 -0
  12. data/lib/aikido/zen/collector/routes.rb +13 -8
  13. data/lib/aikido/zen/collector/stats.rb +16 -19
  14. data/lib/aikido/zen/collector/users.rb +3 -1
  15. data/lib/aikido/zen/collector.rb +94 -29
  16. data/lib/aikido/zen/config.rb +4 -10
  17. data/lib/aikido/zen/context.rb +8 -7
  18. data/lib/aikido/zen/detached_agent/agent.rb +28 -27
  19. data/lib/aikido/zen/detached_agent/front_object.rb +10 -6
  20. data/lib/aikido/zen/internals.rb +23 -3
  21. data/lib/aikido/zen/libzen-v0.1.48-arm64-linux-musl.so +0 -0
  22. data/lib/aikido/zen/middleware/fork_detector.rb +23 -0
  23. data/lib/aikido/zen/middleware/request_tracker.rb +1 -1
  24. data/lib/aikido/zen/outbound_connection.rb +7 -0
  25. data/lib/aikido/zen/rails_engine.rb +2 -6
  26. data/lib/aikido/zen/route.rb +7 -0
  27. data/lib/aikido/zen/runtime_settings.rb +1 -1
  28. data/lib/aikido/zen/scanners/path_traversal/helpers.rb +10 -7
  29. data/lib/aikido/zen/scanners/path_traversal_scanner.rb +2 -2
  30. data/lib/aikido/zen/sink.rb +1 -1
  31. data/lib/aikido/zen/sinks/file.rb +43 -4
  32. data/lib/aikido/zen/sinks/kernel.rb +1 -1
  33. data/lib/aikido/zen/version.rb +2 -2
  34. data/lib/aikido/zen.rb +8 -9
  35. metadata +6 -3
  36. data/lib/aikido/zen/libzen-v0.1.39-arm64-linux-musl.so +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b63c4b14ec7d3bd152a081c862281156b30c7558746797fdc0267a363907fe72
4
- data.tar.gz: dd9e643789ac56f6ae4e452113030f333410d9d35a0f429459b4fc1a094baae8
3
+ metadata.gz: f6bd6dc54a4a28f40b169e505d7fa8a5a051ca5f2d6d1468cb478c9e9fde29fd
4
+ data.tar.gz: 61c98e963da15f5ef50085ef0695492f12230a89ddca39d104e7716cfa4ad4a6
5
5
  SHA512:
6
- metadata.gz: f9c3579784219ecc194fa22352c7a230b6173f6061196ba57b2861f078cfebf280c3fbd3449fd2728bfa8a7a015070958bccf3cb3e8ca949ef164e1da0b87bfd
7
- data.tar.gz: f6ce4d637d1b97fe2024adaf39e10a051bd8a29db2c3a0c2c178458d33378291b2c818034c38cc031844057c51e74a7a6e798f5aef256a6de7b4f65b8c6f5d4e
6
+ metadata.gz: 3226ce54914bf9733acf4100ee889bba464538d89233c3ade942b96b5bf4fbddb9a8696cc77e88ae7939bdb4c8b0fb4cfc84ccabd8e35ee590e10cfc0384e83d
7
+ data.tar.gz: 14f342eb8f9430524c941f8d513f1a20f6edfb95f7837c8e7625faf3667269abd16f3866b97e1c6a05cd8543e348065d14f0b41478f4f58dff8b6a61ca6367e0
data/.simplecov CHANGED
@@ -6,6 +6,12 @@
6
6
  return if RUBY_VERSION < "3.0"
7
7
  return if ENV["DISABLE_COVERAGE"] == "true"
8
8
 
9
+ # Output coverage as LCOV to support CodeCov
10
+ if ENV["COVERAGE_OUTPUT_LCOV"] == "true"
11
+ SimpleCov::Formatter::LcovFormatter.config.report_with_single_file = true
12
+ SimpleCov.formatter = SimpleCov::Formatter::LcovFormatter
13
+ end
14
+
9
15
  SimpleCov.start do
10
16
  # Make sure SimpleCov waits until after the tests
11
17
  # are finished to generate the coverage reports.
data/README.md CHANGED
@@ -6,6 +6,7 @@
6
6
  [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](http://makeapullrequest.com)
7
7
  [![Unit tests](https://github.com/AikidoSec/firewall-ruby/actions/workflows/main.yml/badge.svg)](https://github.com/AikidoSec/firewall-ruby/actions/workflows/main.yml)
8
8
  [![Release](https://github.com/AikidoSec/firewall-ruby/actions/workflows/release.yml/badge.svg)](https://github.com/AikidoSec/firewall-ruby/actions/workflows/release.yml)
9
+ [![codecov](https://codecov.io/gh/AikidoSec/firewall-ruby/graph/badge.svg?token=X0MLST7S15)](https://codecov.io/gh/AikidoSec/firewall-ruby)
9
10
 
10
11
  Zen, your in-app firewall for peace of mind - at runtime.
11
12
 
data/benchmarks/README.md CHANGED
@@ -1,6 +1,5 @@
1
1
  # Benchmarking Zen for Ruby
2
2
 
3
-
4
3
  We use [WRK](https://github.com/wg/wrk) & [Grafana K6](https://k6.io) for these.
5
4
 
6
5
  WRK benchmarks are only requesting a URL (`/benchmark`). In case you want to add more
@@ -0,0 +1 @@
1
+ rails7.1_sql_injection.js
@@ -1,5 +1,5 @@
1
- import http from 'k6/http';
2
- import {Trend} from 'k6/metrics';
1
+ import http from "k6/http";
2
+ import {Trend} from "k6/metrics";
3
3
 
4
4
  const HTTP = {
5
5
  withZen: {
@@ -10,7 +10,7 @@ const HTTP = {
10
10
  get: (path, ...args) => http.get("http://localhost:3002" + path, ...args),
11
11
  post: (path, ...args) => http.post("http://localhost:3002" + path, ...args)
12
12
  }
13
- }
13
+ };
14
14
 
15
15
  function test(name, fn) {
16
16
  const withZen = fn(HTTP.withZen);
@@ -36,35 +36,67 @@ function buildTestTrends(prefix) {
36
36
 
37
37
  const tests = {
38
38
  test_post_page_with_json_body: buildTestTrends("test_post_page_with_json_body"),
39
- test_get_page_without_attack: buildTestTrends("test_get_page_without_attack"),
40
- test_get_page_with_sql_injection: buildTestTrends("test_get_page_with_sql_injection")
41
- }
39
+ test_get_page_without_attack: buildTestTrends("test_get_page_without_attack")
40
+ };
41
+
42
42
  export const options = {
43
43
  vus: 1, // Number of virtual users
44
- iterations: 200,
44
+ duration: "60s",
45
45
  thresholds: {
46
- http_req_failed: ['rate==0'], // we are marking the attacks as expected, so we should have no errors
46
+ http_req_failed: ["rate==0"], // We are marking the attacks as expected, so we should have no errors.
47
47
  test_post_page_with_json_body_delta: ["med<10"],
48
- test_get_page_without_attack_delta: ["med<10"],
49
- test_get_page_with_sql_injection_delta: ["med<10"],
48
+ test_get_page_without_attack_delta: ["med<10"]
50
49
  }
51
50
  };
52
51
 
52
+ const headers = {
53
+ Authorization:
54
+ "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.Bw8sSk3kdnT9d803kqqE_LZJzY1PzMl5cbmuanQKxrI",
55
+ Accept:
56
+ "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
57
+ "Accept-Language": "en-US,en;q=0.9",
58
+ Dnt: "1",
59
+ Priority: "u=0, i",
60
+ "Sec-Ch-Ua":
61
+ '"Not)A;Brand";v="99", "Google Chrome";v="127", "Chromium";v="127"',
62
+ "Sec-Ch-Ua-Arch": '"arm"',
63
+ "Sec-Ch-Ua-Bitness": '"64"',
64
+ "Sec-Ch-Ua-Full-Version-List":
65
+ '"Not)A;Brand";v="99.0.0.0", "Google Chrome";v="127.0.6533.72", "Chromium";v="127.0.6533.72"',
66
+ "Sec-Ch-Ua-Mobile": "?0",
67
+ "Sec-Ch-Ua-Model": '""',
68
+ "Sec-Ch-Ua-Platform": '"macOS"',
69
+ "Sec-Ch-Ua-Platform-Version": '"14.5.0"',
70
+ "Sec-Ch-Ua-Wow64": "?0",
71
+ "Sec-Fetch-Dest": "document",
72
+ "Sec-Fetch-Mode": "navigate",
73
+ "Sec-Fetch-Site": "cross-site",
74
+ "Sec-Fetch-User": "?1",
75
+ "Sec-Gpc": "1",
76
+ "Upgrade-Insecure-Requests": "1",
77
+ "User-Agent":
78
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36",
79
+ };
80
+
53
81
  const expectAttack = http.expectedStatuses(200, 500);
54
82
 
55
83
  export default function () {
56
84
  test("test_post_page_with_json_body",
57
- (http) => http.post("/cats", JSON.stringify({cat: {name: "Féline Dion"}}), {
58
- headers: {
59
- "Content-Type": "application/json",
60
- "Accept": "application/json"
61
- }
62
- })
85
+ (http) => http.post("/cats",
86
+ JSON.stringify({cat: {name: "Féline Dion"}}),
87
+ {
88
+ headers: {
89
+ ...headers,
90
+ "Content-Type": "application/json",
91
+ "Accept": "application/json"
92
+ }
93
+ })
63
94
  )
64
95
 
65
- test("test_get_page_without_attack", (http) => http.get("/cats"))
66
-
67
- test("test_get_page_with_sql_injection", (http) =>
68
- http.get("/cats/1'%20OR%20''='", { responseCallback: expectAttack })
96
+ test("test_get_page_without_attack",
97
+ (http) => http.get("/cats/count",
98
+ {
99
+ headers: headers
100
+ })
69
101
  )
70
102
  }
data/docs/rails.md CHANGED
@@ -60,7 +60,7 @@ modify in an initializer if desired:
60
60
 
61
61
  ```ruby
62
62
  # config/initializers/zen.rb
63
- Rails.application.config.zen.api_timeouts = 20
63
+ Rails.application.config.zen.option = value
64
64
  ```
65
65
 
66
66
  You can access the configuration object both as `Aikido::Zen.config` or
@@ -80,7 +80,7 @@ zen:
80
80
  token: "AIKIDO_RUNTIME_..."
81
81
  ```
82
82
 
83
- You can just tell Zen to use it like so:
83
+ You can tell Zen to use it like so:
84
84
 
85
85
  ```ruby
86
86
  # config/initializers/zen.rb
@@ -102,13 +102,11 @@ way.
102
102
 
103
103
  ## Logging
104
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:
105
+ You can override the logger to integrate with your application logging strategy:
107
106
 
108
107
  ```ruby
109
108
  # config/initializers/zen.rb
110
- Rails.application.config.zen.logger = Logger.new(...)
109
+ Rails.application.config.zen.logger = ::Rails.logger
111
110
  ```
112
111
 
113
- You should supply an instance of ruby's [Logger](https://github.com/ruby/logger)
114
- class.
112
+ Zen expects an instance of Ruby's [Logger](https://github.com/ruby/logger) class.
@@ -38,6 +38,16 @@ module Aikido::Zen
38
38
 
39
39
  # Represents someone connecting to the application and making requests.
40
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
+
41
51
  # @return [String] a unique identifier for this user.
42
52
  attr_reader :id
43
53
 
@@ -50,18 +60,20 @@ module Aikido::Zen
50
60
  # @param id [String]
51
61
  # @param name [String, nil]
52
62
  # @param ip [String, nil]
53
- # @param seen_at [Time]
63
+ # @param first_seen_at [Time]
64
+ # @param last_seen_at [Time]
54
65
  def initialize(
55
66
  id:,
56
67
  name: nil,
57
68
  ip: Aikido::Zen.current_context&.request&.ip,
58
- seen_at: Time.now.utc
69
+ first_seen_at: Time.now.utc,
70
+ last_seen_at: first_seen_at
59
71
  )
60
72
  @id = id
61
73
  @name = name
62
- @first_seen_at = seen_at
63
- @last_seen_at = Concurrent::AtomicReference.new(seen_at)
64
74
  @ip = Concurrent::AtomicReference.new(ip)
75
+ @first_seen_at = first_seen_at
76
+ @last_seen_at = Concurrent::AtomicReference.new(last_seen_at)
65
77
  end
66
78
 
67
79
  # @return [Time]
@@ -89,6 +101,24 @@ module Aikido::Zen
89
101
  @ip.try_update { |last_ip| [ip, last_ip].compact.first }
90
102
  end
91
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
+
92
122
  # @return [self]
93
123
  def to_aikido_actor
94
124
  self
@@ -20,7 +20,7 @@ module Aikido::Zen
20
20
  # @return [Boolean] whether the currently running heartbeat matches the
21
21
  # expected interval in the runtime settings.
22
22
  def stale_settings?
23
- running? && @timer.execution_interval != @settings.heartbeat_interval
23
+ running? && @timer.execution_interval != interval
24
24
  end
25
25
 
26
26
  # Sets up the the timer to run the given block at the appropriate interval.
@@ -30,11 +30,11 @@ module Aikido::Zen
30
30
  def start(&task)
31
31
  return if running?
32
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)
33
+ if interval&.nonzero?
34
+ @config.logger.debug "Scheduling heartbeats every #{interval} seconds"
35
+ @timer = @worker.every(interval, run_now: false, &task)
36
36
  else
37
- @config.logger.warn(format("Heartbeat could not be set up (interval: %p)", @settings.heartbeat_interval))
37
+ @config.logger.warn(format("Heartbeat could not be set up (interval: %p)", interval))
38
38
  end
39
39
  end
40
40
 
@@ -37,22 +37,22 @@ module Aikido::Zen
37
37
  end
38
38
 
39
39
  def start!
40
- @config.logger.info "Starting Aikido agent v#{Aikido::Zen::VERSION}"
40
+ @config.logger.info("Starting Aikido agent v#{Aikido::Zen::VERSION}")
41
41
 
42
42
  raise Aikido::ZenError, "Aikido Agent already started!" if started?
43
43
  @started_at = Time.now.utc
44
44
  @collector.start(at: @started_at)
45
45
 
46
46
  if @config.blocking_mode?
47
- @config.logger.info "Requests identified as attacks will be blocked"
47
+ @config.logger.info("Requests identified as attacks will be blocked")
48
48
  else
49
- @config.logger.warn "Non-blocking mode enabled! No requests will be blocked."
49
+ @config.logger.warn("Non-blocking mode enabled! No requests will be blocked.")
50
50
  end
51
51
 
52
52
  if @api_client.can_make_requests?
53
- @config.logger.info "API Token set! Reporting has been enabled."
53
+ @config.logger.info("API Token set! Reporting has been enabled.")
54
54
  else
55
- @config.logger.warn "No API Token set! Reporting has been disabled."
55
+ @config.logger.warn("No API Token set! Reporting has been disabled.")
56
56
  return
57
57
  end
58
58
 
@@ -60,15 +60,18 @@ module Aikido::Zen
60
60
 
61
61
  report(Events::Started.new(time: @started_at)) do |response|
62
62
  updated_settings! if Aikido::Zen.runtime_settings.update_from_json(response)
63
- @config.logger.info "Updated runtime settings."
63
+ @config.logger.info("Updated runtime settings.")
64
64
  rescue => err
65
65
  @config.logger.error(err.message)
66
66
  end
67
67
 
68
68
  poll_for_setting_updates
69
69
 
70
- @worker.delay(@config.initial_heartbeat_delay) do
71
- send_heartbeat if @collector.stats.any?
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
72
75
  end
73
76
  end
74
77
 
@@ -77,7 +80,7 @@ module Aikido::Zen
77
80
  #
78
81
  # @return [void]
79
82
  def stop!
80
- @config.logger.info "Stopping Aikido agent"
83
+ @config.logger.info("Stopping Aikido agent")
81
84
  @started_at = nil
82
85
  @worker.shutdown
83
86
  end
@@ -143,11 +146,10 @@ module Aikido::Zen
143
146
  def send_heartbeat(at: Time.now.utc)
144
147
  return unless @api_client.can_make_requests?
145
148
 
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
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")
151
153
  end
152
154
  end
153
155
 
@@ -162,7 +164,7 @@ module Aikido::Zen
162
164
  @worker.every(@config.polling_interval) do
163
165
  if @api_client.should_fetch_settings?
164
166
  updated_settings! if Aikido::Zen.runtime_settings.update_from_json(@api_client.fetch_settings)
165
- @config.logger.info "Updated runtime settings after polling"
167
+ @config.logger.info("Updated runtime settings after polling")
166
168
  end
167
169
  end
168
170
  end
@@ -0,0 +1,209 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikido::Zen
4
+ class Collector
5
+ class Event
6
+ @@registry = {}
7
+
8
+ # @api protected
9
+ def self.register(type)
10
+ const_set(:TYPE, type)
11
+ @@registry[type] = self
12
+ end
13
+
14
+ def self.from_json(data)
15
+ type = data[:type]
16
+ subclass = @@registry[type]
17
+ subclass.from_json(data)
18
+ end
19
+
20
+ attr_reader :type
21
+
22
+ def initialize
23
+ @type = self.class::TYPE
24
+ end
25
+
26
+ def as_json
27
+ {
28
+ type: @type
29
+ }
30
+ end
31
+
32
+ def handle(collector)
33
+ raise NotImplementedError, "implement in subclasses"
34
+ end
35
+ end
36
+
37
+ # @api private
38
+ module Events
39
+ class TrackRequest < Event
40
+ register "track_request"
41
+
42
+ def self.from_json(data)
43
+ new
44
+ end
45
+
46
+ def handle(collector)
47
+ collector.handle_track_request
48
+ end
49
+
50
+ def inspect
51
+ "#<#{self.class.name}>"
52
+ end
53
+ end
54
+
55
+ class TrackScan < Event
56
+ register "track_scan"
57
+
58
+ def self.from_json(data)
59
+ new(
60
+ data[:sink_name],
61
+ data[:duration],
62
+ has_errors: data[:has_errors]
63
+ )
64
+ end
65
+
66
+ def initialize(sink_name, duration, has_errors:)
67
+ super()
68
+ @sink_name = sink_name
69
+ @duration = duration
70
+ @has_errors = has_errors
71
+ end
72
+
73
+ def as_json
74
+ super.update({
75
+ sink_name: @sink_name,
76
+ duration: @duration,
77
+ has_errors: @has_errors
78
+ })
79
+ end
80
+
81
+ def handle(collector)
82
+ collector.handle_track_scan(@sink_name, @duration, has_errors: @has_errors)
83
+ end
84
+
85
+ def inspect
86
+ "#<#{self.class.name} #{@sink_name} #{format "%0.6f", @duration} #{@has_errors}>"
87
+ end
88
+ end
89
+
90
+ class TrackAttack < Event
91
+ register "track_attack"
92
+
93
+ def self.from_json(data)
94
+ new(
95
+ data[:sink_name],
96
+ being_blocked: data[:being_blocked]
97
+ )
98
+ end
99
+
100
+ def initialize(sink_name, being_blocked:)
101
+ super()
102
+ @sink_name = sink_name
103
+ @being_blocked = being_blocked
104
+ end
105
+
106
+ def as_json
107
+ super.update({
108
+ sink_name: @sink_name,
109
+ being_blocked: @being_blocked
110
+ })
111
+ end
112
+
113
+ def handle(collector)
114
+ collector.handle_track_attack(@sink_name, being_blocked: @being_blocked)
115
+ end
116
+
117
+ def inspect
118
+ "#<#{self.class.name} #{@sink_name} #{@being_blocked}>"
119
+ end
120
+ end
121
+
122
+ class TrackUser < Event
123
+ register "track_user"
124
+
125
+ def self.from_json(data)
126
+ new(Aikido::Zen::Actor.from_json(data[:actor]))
127
+ end
128
+
129
+ def initialize(actor)
130
+ super()
131
+ @actor = actor
132
+ end
133
+
134
+ def as_json
135
+ super.update({
136
+ actor: @actor.as_json
137
+ })
138
+ end
139
+
140
+ def handle(collector)
141
+ collector.handle_track_user(@actor)
142
+ end
143
+
144
+ def inspect
145
+ "#<#{self.class.name} #{@actor.id} #{@actor.name}>"
146
+ end
147
+ end
148
+
149
+ class TrackOutbound < Event
150
+ register "track_outbound"
151
+
152
+ def self.from_json(data)
153
+ new(OutboundConnection.from_json(data[:connection]))
154
+ end
155
+
156
+ def initialize(connection)
157
+ super()
158
+ @connection = connection
159
+ end
160
+
161
+ def as_json
162
+ super.update({
163
+ connection: @connection.as_json
164
+ })
165
+ end
166
+
167
+ def handle(collector)
168
+ collector.handle_track_outbound(@connection)
169
+ end
170
+
171
+ def inspect
172
+ "#<#{self.class.name} #{@connection.host}:#{@connection.port}>"
173
+ end
174
+ end
175
+
176
+ class TrackRoute < Event
177
+ register "track_route"
178
+
179
+ def self.from_json(data)
180
+ new(
181
+ Route.from_json(data[:route]),
182
+ Request::Schema.from_json(data[:schema])
183
+ )
184
+ end
185
+
186
+ def initialize(route, schema)
187
+ super()
188
+ @route = route
189
+ @schema = schema
190
+ end
191
+
192
+ def as_json
193
+ super.update({
194
+ route: @route.as_json,
195
+ schema: @schema.as_json
196
+ })
197
+ end
198
+
199
+ def handle(collector)
200
+ collector.handle_track_route(@route, @schema)
201
+ end
202
+
203
+ def inspect
204
+ "#<#{self.class.name} #{@route.verb} #{@route.path.inspect}>"
205
+ end
206
+ end
207
+ end
208
+ end
209
+ end
@@ -7,6 +7,8 @@ module Aikido::Zen
7
7
  #
8
8
  # Keeps track of the visited routes.
9
9
  class Collector::Routes
10
+ # @api private
11
+ # Visible for testing.
10
12
  attr_reader :visits
11
13
 
12
14
  def initialize(config = Aikido::Zen.config)
@@ -14,11 +16,11 @@ module Aikido::Zen
14
16
  @visits = Hash.new { |h, k| h[k] = Record.new }
15
17
  end
16
18
 
17
- # @param request [Aikido::Zen::Request].
18
- # @return [self]
19
- def add(request)
20
- @visits[request.route].increment(request) unless request.route.nil?
21
- self
19
+ # @param route [Aikido::Zen::Route] the route of the request
20
+ # @param schema [Aikido::Zen::Request::Schema] the schema for the request
21
+ # @return [void]
22
+ def add(route, schema)
23
+ @visits[route].increment(schema) unless route.nil?
22
24
  end
23
25
 
24
26
  def as_json
@@ -33,6 +35,7 @@ module Aikido::Zen
33
35
  end
34
36
 
35
37
  # @api private
38
+ # Visible for testing.
36
39
  def [](route)
37
40
  @visits[route]
38
41
  end
@@ -49,16 +52,18 @@ module Aikido::Zen
49
52
  @config = config
50
53
  end
51
54
 
52
- def increment(request)
55
+ def increment(schema)
53
56
  self.hits += 1
54
57
 
55
58
  if sample_schema?
56
59
  self.samples += 1
57
- self.schema |= request.schema
60
+ self.schema |= schema
58
61
  end
59
62
  end
60
63
 
61
- private def sample_schema?
64
+ private
65
+
66
+ def sample_schema?
62
67
  samples < @config.api_schema_max_samples
63
68
  end
64
69
  end