aikido-zen 1.5.1 → 1.6.0.beta.2

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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/benchmarks/ipc/benchmark.rb +149 -0
  3. data/benchmarks/rpc/benchmark.rb +57 -0
  4. data/docs/rails.md +22 -0
  5. data/lib/aikido/zen/actor.rb +10 -10
  6. data/lib/aikido/zen/agent.rb +2 -2
  7. data/lib/aikido/zen/api_cache.rb +8 -0
  8. data/lib/aikido/zen/attack.rb +14 -14
  9. data/lib/aikido/zen/attack_wave.rb +8 -8
  10. data/lib/aikido/zen/collector/event.rb +26 -26
  11. data/lib/aikido/zen/collector/routes.rb +4 -4
  12. data/lib/aikido/zen/collector/sink_stats.rb +10 -10
  13. data/lib/aikido/zen/collector/stats.rb +17 -17
  14. data/lib/aikido/zen/config.rb +45 -25
  15. data/lib/aikido/zen/errors.rb +0 -8
  16. data/lib/aikido/zen/event.rb +12 -12
  17. data/lib/aikido/zen/ipc/ipc.rb +506 -0
  18. data/lib/aikido/zen/ipc/rpc.rb +349 -0
  19. data/lib/aikido/zen/ipc.rb +4 -0
  20. data/lib/aikido/zen/middleware/fork_detector.rb +8 -2
  21. data/lib/aikido/zen/middleware/rack_throttler.rb +2 -4
  22. data/lib/aikido/zen/outbound_connection.rb +3 -3
  23. data/lib/aikido/zen/payload.rb +3 -3
  24. data/lib/aikido/zen/rate_limiter/result.rb +20 -0
  25. data/lib/aikido/zen/request.rb +14 -9
  26. data/lib/aikido/zen/route.rb +3 -3
  27. data/lib/aikido/zen/sinks/action_controller.rb +2 -4
  28. data/lib/aikido/zen/system_info.rb +13 -13
  29. data/lib/aikido/zen/version.rb +1 -1
  30. data/lib/aikido/zen/worker_process/agent/client.rb +112 -0
  31. data/lib/aikido/zen/worker_process/agent/server.rb +88 -0
  32. data/lib/aikido/zen/worker_process/agent.rb +4 -0
  33. data/lib/aikido/zen/worker_process.rb +3 -0
  34. data/lib/aikido/zen.rb +55 -29
  35. metadata +12 -6
  36. data/lib/aikido/zen/detached_agent/agent.rb +0 -79
  37. data/lib/aikido/zen/detached_agent/front_object.rb +0 -41
  38. data/lib/aikido/zen/detached_agent/server.rb +0 -78
  39. data/lib/aikido/zen/detached_agent.rb +0 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 060fe3aabca1fbc463fc77129a0b535d6c53f2c78bf5c4d9d11c2bdd46d119be
4
- data.tar.gz: 1fa12dcaed6348cfc9ed836e04c995f2459c46b23c8ba5c529fe5fa0c01da40e
3
+ metadata.gz: 738c94bd7530220f9d871478c41d7108a8c36d347ef16a62f6895bd94854ef6d
4
+ data.tar.gz: 6bcd92246a8b763cd6294b75ac03421cf0ff2c7a67d1088c1107250724ee11cd
5
5
  SHA512:
6
- metadata.gz: c8d1311f2b1ab68a068b5aadf5356f6d092ac9df323030c7143d042e9a9705cf7efa06efcf7f15804c7174f78543ad0d630d53b47d42fce487de8f2eeec344eb
7
- data.tar.gz: cd12d37d110b93b2603f87897985f1d2e70a98a9438c6bdd8beb7f5adfb0a86a4f179a925528472da8af5d859d88309c61cbdd2aa94baa029636f4cc0870c813
6
+ metadata.gz: 89377ac769259219f973ff26d4a123fe74ef865d5ec9bd1c2ba0d035e4f2ab49217cffd77828b7c5a5d47b34e6255f9a719ff7b23cd2f7d00cf332dd97a0d0ef
7
+ data.tar.gz: 6124153d75e81e9879e2eb7910a4996b1d464f87cc35381efcaeea85b3dcb3fe70d20893ebfa541d3319606896ed3db13aaa72b214f1527ee02accfe18318360
@@ -0,0 +1,149 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "benchmark"
4
+ require "securerandom"
5
+ require "aikido-zen"
6
+
7
+ class BasicIO
8
+ def initialize(data, timeout: 5)
9
+ @data = data
10
+ @size = data.bytesize
11
+ @timeout = timeout
12
+ end
13
+
14
+ def read(socket)
15
+ buffer = String.new(encoding: Encoding::BINARY)
16
+
17
+ while buffer.bytesize < @size
18
+ case chunk = socket.read_nonblock(@size - buffer.bytesize, exception: false)
19
+ when :wait_readable
20
+ raise Errno::ETIMEDOUT, "read timed out" unless IO.select([socket], nil, nil, @timeout)
21
+ when nil
22
+ raise EOFError
23
+ else
24
+ buffer << chunk
25
+ end
26
+ end
27
+
28
+ buffer
29
+ end
30
+
31
+ def write(socket)
32
+ written = 0
33
+
34
+ while written < @data.bytesize
35
+ case n = socket.write_nonblock(@data.byteslice(written..), exception: false)
36
+ when :wait_writable
37
+ raise Errno::ETIMEDOUT, "write timed out" unless IO.select(nil, [socket], nil, @timeout)
38
+ else
39
+ written += n
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ class TimedIO
46
+ include Aikido::Zen::IPC::TimedIO
47
+
48
+ def initialize(data, timeout: 5)
49
+ @data = data
50
+ @size = data.bytesize
51
+ @timeout = timeout
52
+ end
53
+
54
+ def read(socket)
55
+ read_with_deadline(socket, @size, Process.clock_gettime(Process::CLOCK_MONOTONIC) + @timeout)
56
+ end
57
+
58
+ def write(socket)
59
+ write_with_deadline(socket, @data, Process.clock_gettime(Process::CLOCK_MONOTONIC) + @timeout)
60
+ end
61
+ end
62
+
63
+ class FramedIO
64
+ include Aikido::Zen::IPC::FramedIO
65
+
66
+ def initialize(data, timeout: 5)
67
+ @data = data
68
+ @timeout = timeout
69
+ @buffer = String.new(encoding: Encoding::BINARY)
70
+ end
71
+
72
+ def read(socket)
73
+ read_coalesced_frame_with_timeout(socket, @buffer, nil, @timeout)
74
+ end
75
+
76
+ def write(socket)
77
+ write_coalesced_frame_with_timeout(socket, @data, nil, @timeout)
78
+ end
79
+ end
80
+
81
+ def run(duration, io)
82
+ secret = SecureRandom.bytes(32)
83
+
84
+ server = Aikido::Zen::IPC::Server.start(secret) do |socket|
85
+ loop do
86
+ io.read(socket)
87
+ end
88
+ rescue EOFError, Errno::ECONNRESET, Errno::EPIPE
89
+ # disconnected
90
+ rescue IOError
91
+ # client stopped
92
+ end
93
+
94
+ client = Aikido::Zen::IPC::Client.new(secret, server.host, server.port)
95
+
96
+ sent = 0
97
+
98
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + duration
99
+
100
+ result = Benchmark.measure do
101
+ while Process.clock_gettime(Process::CLOCK_MONOTONIC) < deadline
102
+ io.write(client.socket)
103
+
104
+ sent += 1
105
+ end
106
+ end
107
+
108
+ [sent, result.real]
109
+ ensure
110
+ client.close
111
+ server.stop
112
+ end
113
+
114
+ def report(label, sent, real, data_size, baseline: nil)
115
+ rate = sent / real
116
+ mib_per_sec = rate * data_size / (1024 * 1024)
117
+
118
+ puts label
119
+ puts "#{rate.round} messages/sec, #{mib_per_sec.round(2)} MiB/sec"
120
+
121
+ if baseline
122
+ overhead = 100 - (100.0 * rate / baseline)
123
+ puts "#{overhead.round(1)}% fewer messages/sec than baseline"
124
+ end
125
+ end
126
+
127
+ if __FILE__ == $0
128
+ duration = (ENV["DURATION"] || 5).to_i
129
+ data_size = (ENV["DATA_SIZE"] || 1024).to_i
130
+
131
+ data = SecureRandom.bytes(data_size)
132
+
133
+ basic_io = BasicIO.new(data)
134
+ basic_io_sent, basic_io_real = run(duration, basic_io)
135
+ report("baseline", basic_io_sent, basic_io_real, data_size)
136
+ basic_io_rate = basic_io_sent / basic_io_real
137
+
138
+ puts
139
+
140
+ timed_io = TimedIO.new(data)
141
+ timed_io_sent, timed_io_real = run(duration, timed_io)
142
+ report("TimedIO", timed_io_sent, timed_io_real, data_size, baseline: basic_io_rate)
143
+
144
+ puts
145
+
146
+ framed_io = FramedIO.new(data)
147
+ framed_io_sent, framed_io_real = run(duration, framed_io)
148
+ report("FramedIO (coalesced)", framed_io_sent, framed_io_real, data_size, baseline: basic_io_rate)
149
+ end
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "benchmark"
4
+ require "securerandom"
5
+ require "logger"
6
+ require "aikido-zen"
7
+
8
+ def run(duration, concurrency)
9
+ secret = SecureRandom.bytes(32)
10
+ logger = Logger.new(File::NULL)
11
+
12
+ server = Aikido::Zen::RPC::Server.start(secret, logger: logger) do |server|
13
+ server.handle("echo") { |respond, value| respond.call(value) }
14
+ end
15
+
16
+ client = Aikido::Zen::RPC::Client.start(secret, server.host, server.port, logger: logger)
17
+
18
+ calls = Concurrent::AtomicFixnum.new(0)
19
+
20
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + duration
21
+
22
+ result = Benchmark.measure do
23
+ threads = concurrency.times.map do
24
+ Thread.new do
25
+ while Process.clock_gettime(Process::CLOCK_MONOTONIC) < deadline
26
+ client.invoke("echo", 5.0, "hello")
27
+
28
+ calls.increment
29
+ end
30
+ end
31
+ end
32
+
33
+ threads.each(&:join)
34
+ end
35
+
36
+ [calls.value, result.real]
37
+ ensure
38
+ client.stop
39
+ server.stop
40
+ end
41
+
42
+ def report(label, total_calls, real)
43
+ rate = total_calls / real
44
+ latency_ms = real / total_calls * 1000
45
+
46
+ puts label
47
+ puts "#{rate.round} calls/sec, #{latency_ms.round(3)} ms/call"
48
+ end
49
+
50
+ if __FILE__ == $0
51
+ duration = (ENV["DURATION"] || 5).to_i
52
+ concurrency = (ENV["CONCURRENCY"] || 1).to_i
53
+
54
+ calls, real = run(duration, concurrency)
55
+
56
+ report("RPC echo (concurrency: #{concurrency})", calls, real)
57
+ end
data/docs/rails.md CHANGED
@@ -27,6 +27,28 @@ Bundler.require(*Rails.groups)
27
27
  That's it! Zen will start to run inside your app when it starts getting
28
28
  requests.
29
29
 
30
+ ## Running with multiple worker processes (Puma)
31
+
32
+ Zen can share rate limiting state, runtime settings, and runtime stats
33
+ across all of an app's worker processes. This only works if Rails is booted
34
+ once and then forked into workers, which on Puma requires
35
+ [`preload_app!`][puma-preload]:
36
+
37
+ ```ruby
38
+ # config/puma.rb
39
+ preload_app!
40
+ ```
41
+
42
+ Without `preload_app!`, each worker boots Rails independently, so Zen
43
+ tracks rate limits, settings, and stats separately per worker instead
44
+ of sharing them.
45
+
46
+ > [!NOTE]
47
+ > This only matters for multi-process setups; it doesn't apply to
48
+ > single-process or threaded-only deployments.
49
+
50
+ [puma-preload]: https://github.com/puma/puma#cluster-mode
51
+
30
52
  ## Rate limiting and user blocking
31
53
 
32
54
  If you want to add the rate limiting feature to your app, modify your code like this:
@@ -40,11 +40,11 @@ module Aikido::Zen
40
40
  class Actor
41
41
  def self.from_json(data)
42
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)
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
48
  )
49
49
  end
50
50
 
@@ -135,11 +135,11 @@ module Aikido::Zen
135
135
 
136
136
  def as_json
137
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
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
143
  }.compact
144
144
  end
145
145
  end
@@ -19,14 +19,12 @@ module Aikido::Zen
19
19
  def initialize(
20
20
  config: Aikido::Zen.config,
21
21
  collector: Aikido::Zen.collector,
22
- detached_agent: Aikido::Zen.detached_agent,
23
22
  worker: Aikido::Zen::Worker.new(config: config),
24
23
  api_client: Aikido::Zen::APIClient.new(config: config),
25
24
  api_stream: Aikido::Zen::APIStream.new(config: config)
26
25
  )
27
26
  @config = config
28
27
  @collector = collector
29
- @detached_agent = detached_agent
30
28
  @worker = worker
31
29
  @api_client = api_client
32
30
  @api_stream = api_stream
@@ -235,6 +233,7 @@ module Aikido::Zen
235
233
  def update_settings_from_runtime_config!(data)
236
234
  return unless @runtime_config_update_mutex.try_lock
237
235
  begin
236
+ Aikido::Zen.api_cache.runtime_config = data
238
237
  Aikido::Zen.runtime_settings.update_from_runtime_config_json(data)
239
238
  ensure
240
239
  @runtime_config_update_mutex.unlock
@@ -246,6 +245,7 @@ module Aikido::Zen
246
245
  def update_settings_from_runtime_firewall_lists!(data)
247
246
  return unless @runtime_firewall_lists_update_mutex.try_lock
248
247
  begin
248
+ Aikido::Zen.api_cache.runtime_firewall_lists = data
249
249
  Aikido::Zen.runtime_settings.update_from_runtime_firewall_lists_json(data)
250
250
  ensure
251
251
  @runtime_firewall_lists_update_mutex.unlock
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikido::Zen
4
+ class APICache
5
+ attr_accessor :runtime_config
6
+ attr_accessor :runtime_firewall_lists
7
+ end
8
+ end
@@ -44,11 +44,11 @@ module Aikido::Zen
44
44
 
45
45
  def as_json
46
46
  {
47
- kind: kind,
48
- blocked: blocked?,
49
- metadata: metadata,
50
- operation: @operation,
51
- stack: @stack
47
+ "kind" => kind,
48
+ "blocked" => blocked?,
49
+ "metadata" => metadata,
50
+ "operation" => @operation,
51
+ "stack" => @stack
52
52
  }.compact.merge(input.as_json)
53
53
  end
54
54
 
@@ -70,7 +70,7 @@ module Aikido::Zen
70
70
 
71
71
  def metadata
72
72
  {
73
- filename: filepath
73
+ "filename" => filepath
74
74
  }
75
75
  end
76
76
 
@@ -107,7 +107,7 @@ module Aikido::Zen
107
107
 
108
108
  def metadata
109
109
  {
110
- command: @command
110
+ "command" => @command
111
111
  }
112
112
  end
113
113
 
@@ -139,9 +139,9 @@ module Aikido::Zen
139
139
 
140
140
  def metadata
141
141
  {
142
- sql: @query,
143
- dialect: @dialect.name,
144
- failedToTokenize: @failed_to_tokenize || nil
142
+ "sql" => @query,
143
+ "dialect" => @dialect.name,
144
+ "failedToTokenize" => @failed_to_tokenize || nil
145
145
  }.compact
146
146
  end
147
147
 
@@ -174,8 +174,8 @@ module Aikido::Zen
174
174
 
175
175
  def metadata
176
176
  {
177
- hostname: @request.uri.hostname,
178
- port: @request.uri.port.to_s
177
+ "hostname" => @request.uri.hostname,
178
+ "port" => @request.uri.port.to_s
179
179
  }
180
180
  end
181
181
  end
@@ -212,8 +212,8 @@ module Aikido::Zen
212
212
 
213
213
  def metadata
214
214
  {
215
- hostname: @hostname,
216
- privateIP: @address
215
+ "hostname" => @hostname,
216
+ "privateIP" => @address
217
217
  }
218
218
  end
219
219
  end
@@ -69,9 +69,9 @@ module Aikido::Zen
69
69
 
70
70
  def as_json
71
71
  {
72
- ipAddress: @ip_address,
73
- userAgent: @user_agent,
74
- source: @source
72
+ "ipAddress" => @ip_address,
73
+ "userAgent" => @user_agent,
74
+ "source" => @source
75
75
  }.compact
76
76
  end
77
77
 
@@ -101,10 +101,10 @@ module Aikido::Zen
101
101
 
102
102
  def as_json
103
103
  {
104
- metadata: {
105
- samples: @samples.as_json.to_json # The API only accepts string values in metadata
104
+ "metadata" => {
105
+ "samples" => @samples.as_json.to_json # The API only accepts string values in metadata
106
106
  },
107
- user: @user.as_json
107
+ "user" => @user.as_json
108
108
  }.compact
109
109
  end
110
110
 
@@ -130,8 +130,8 @@ module Aikido::Zen
130
130
 
131
131
  def as_json
132
132
  {
133
- method: @verb.as_json,
134
- url: @path.as_json
133
+ "method" => @verb.as_json,
134
+ "url" => @path.as_json
135
135
  }.compact
136
136
  end
137
137
 
@@ -12,7 +12,7 @@ module Aikido::Zen
12
12
  end
13
13
 
14
14
  def self.from_json(data)
15
- type = data[:type]
15
+ type = data["type"]
16
16
  subclass = @@registry[type]
17
17
  subclass.from_json(data)
18
18
  end
@@ -25,7 +25,7 @@ module Aikido::Zen
25
25
 
26
26
  def as_json
27
27
  {
28
- type: @type
28
+ "type" => @type
29
29
  }
30
30
  end
31
31
 
@@ -72,7 +72,7 @@ module Aikido::Zen
72
72
  register "track_user_agent"
73
73
 
74
74
  def self.from_json(data)
75
- new(data[:user_agent_keys])
75
+ new(data["user_agent_keys"])
76
76
  end
77
77
 
78
78
  def initialize(user_agent_keys)
@@ -82,7 +82,7 @@ module Aikido::Zen
82
82
 
83
83
  def as_json
84
84
  super.update({
85
- user_agent_keys: @user_agent_keys
85
+ "user_agent_keys" => @user_agent_keys
86
86
  })
87
87
  end
88
88
 
@@ -99,7 +99,7 @@ module Aikido::Zen
99
99
  register "track_ip_list"
100
100
 
101
101
  def self.from_json(data)
102
- new(data[:ip_list_keys])
102
+ new(data["ip_list_keys"])
103
103
  end
104
104
 
105
105
  def initialize(ip_list_keys)
@@ -109,7 +109,7 @@ module Aikido::Zen
109
109
 
110
110
  def as_json
111
111
  super.update({
112
- ip_list_keys: @ip_list_keys
112
+ "ip_list_keys" => @ip_list_keys
113
113
  })
114
114
  end
115
115
 
@@ -127,7 +127,7 @@ module Aikido::Zen
127
127
 
128
128
  def self.from_json(data)
129
129
  new(
130
- being_blocked: data[:being_blocked]
130
+ being_blocked: data["being_blocked"]
131
131
  )
132
132
  end
133
133
 
@@ -138,7 +138,7 @@ module Aikido::Zen
138
138
 
139
139
  def as_json
140
140
  super.update({
141
- being_blocked: @being_blocked
141
+ "being_blocked" => @being_blocked
142
142
  })
143
143
  end
144
144
 
@@ -156,9 +156,9 @@ module Aikido::Zen
156
156
 
157
157
  def self.from_json(data)
158
158
  new(
159
- data[:sink_name],
160
- data[:duration],
161
- has_errors: data[:has_errors]
159
+ data["sink_name"],
160
+ data["duration"],
161
+ has_errors: data["has_errors"]
162
162
  )
163
163
  end
164
164
 
@@ -171,9 +171,9 @@ module Aikido::Zen
171
171
 
172
172
  def as_json
173
173
  super.update({
174
- sink_name: @sink_name,
175
- duration: @duration,
176
- has_errors: @has_errors
174
+ "sink_name" => @sink_name,
175
+ "duration" => @duration,
176
+ "has_errors" => @has_errors
177
177
  })
178
178
  end
179
179
 
@@ -191,8 +191,8 @@ module Aikido::Zen
191
191
 
192
192
  def self.from_json(data)
193
193
  new(
194
- data[:sink_name],
195
- being_blocked: data[:being_blocked]
194
+ data["sink_name"],
195
+ being_blocked: data["being_blocked"]
196
196
  )
197
197
  end
198
198
 
@@ -204,8 +204,8 @@ module Aikido::Zen
204
204
 
205
205
  def as_json
206
206
  super.update({
207
- sink_name: @sink_name,
208
- being_blocked: @being_blocked
207
+ "sink_name" => @sink_name,
208
+ "being_blocked" => @being_blocked
209
209
  })
210
210
  end
211
211
 
@@ -222,7 +222,7 @@ module Aikido::Zen
222
222
  register "track_user"
223
223
 
224
224
  def self.from_json(data)
225
- new(Aikido::Zen::Actor.from_json(data[:actor]))
225
+ new(Aikido::Zen::Actor.from_json(data["actor"]))
226
226
  end
227
227
 
228
228
  def initialize(actor)
@@ -232,7 +232,7 @@ module Aikido::Zen
232
232
 
233
233
  def as_json
234
234
  super.update({
235
- actor: @actor.as_json
235
+ "actor" => @actor.as_json
236
236
  })
237
237
  end
238
238
 
@@ -249,7 +249,7 @@ module Aikido::Zen
249
249
  register "track_outbound"
250
250
 
251
251
  def self.from_json(data)
252
- new(OutboundConnection.from_json(data[:connection]))
252
+ new(OutboundConnection.from_json(data["connection"]))
253
253
  end
254
254
 
255
255
  def initialize(connection)
@@ -259,7 +259,7 @@ module Aikido::Zen
259
259
 
260
260
  def as_json
261
261
  super.update({
262
- connection: @connection.as_json
262
+ "connection" => @connection.as_json
263
263
  })
264
264
  end
265
265
 
@@ -277,8 +277,8 @@ module Aikido::Zen
277
277
 
278
278
  def self.from_json(data)
279
279
  new(
280
- Route.from_json(data[:route]),
281
- Request::Schema.from_json(data[:schema])
280
+ Route.from_json(data["route"]),
281
+ Request::Schema.from_json(data["schema"])
282
282
  )
283
283
  end
284
284
 
@@ -290,8 +290,8 @@ module Aikido::Zen
290
290
 
291
291
  def as_json
292
292
  super.update({
293
- route: @route.as_json,
294
- schema: @schema.as_json
293
+ "route" => @route.as_json,
294
+ "schema" => @schema.as_json
295
295
  })
296
296
  end
297
297
 
@@ -26,10 +26,10 @@ module Aikido::Zen
26
26
  def as_json
27
27
  @visits.map do |route, record|
28
28
  {
29
- method: route.verb,
30
- path: route.path,
31
- hits: record.hits,
32
- apispec: record.schema.as_json
29
+ "method" => route.verb,
30
+ "path" => route.path,
31
+ "hits" => record.hits,
32
+ "apispec" => record.schema.as_json
33
33
  }.compact
34
34
  end
35
35
  end
@@ -62,14 +62,14 @@ module Aikido::Zen
62
62
 
63
63
  def as_json
64
64
  {
65
- total: @scans,
66
- interceptorThrewError: @errors,
67
- withoutContext: 0,
68
- attacksDetected: {
69
- total: @attacks,
70
- blocked: @blocked_attacks
65
+ "total" => @scans,
66
+ "interceptorThrewError" => @errors,
67
+ "withoutContext" => 0,
68
+ "attacksDetected" => {
69
+ "total" => @attacks,
70
+ "blocked" => @blocked_attacks
71
71
  },
72
- compressedTimings: @compressed_timings.as_json
72
+ "compressedTimings" => @compressed_timings.as_json
73
73
  }
74
74
  end
75
75
 
@@ -85,9 +85,9 @@ module Aikido::Zen
85
85
  CompressedTiming = Struct.new(:mean, :percentiles, :compressed_at) do
86
86
  def as_json
87
87
  {
88
- averageInMs: mean * 1000,
89
- percentiles: percentiles.transform_values { |t| t * 1000 },
90
- compressedAt: compressed_at.to_i * 1000
88
+ "averageInMs" => mean * 1000,
89
+ "percentiles" => percentiles.transform_values { |t| t * 1000 },
90
+ "compressedAt" => compressed_at.to_i * 1000
91
91
  }
92
92
  end
93
93
  end