aikido-zen 1.0.1.beta.4-arm64-linux-musl → 1.0.2.beta.1-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.
- checksums.yaml +4 -4
- data/docs/rails.md +49 -5
- data/lib/aikido/zen/config.rb +3 -4
- data/lib/aikido/zen/detached_agent/agent.rb +1 -1
- data/lib/aikido/zen/detached_agent/server.rb +63 -26
- data/lib/aikido/zen/sinks/async_http.rb +40 -42
- data/lib/aikido/zen/sinks/curb.rb +56 -58
- data/lib/aikido/zen/sinks/em_http.rb +27 -29
- data/lib/aikido/zen/sinks/excon.rb +62 -65
- data/lib/aikido/zen/sinks/file.rb +66 -70
- data/lib/aikido/zen/sinks/http.rb +26 -28
- data/lib/aikido/zen/sinks/httpclient.rb +27 -29
- data/lib/aikido/zen/sinks/httpx.rb +27 -29
- data/lib/aikido/zen/sinks/kernel.rb +11 -12
- data/lib/aikido/zen/sinks/mysql2.rb +10 -12
- data/lib/aikido/zen/sinks/net_http.rb +25 -27
- data/lib/aikido/zen/sinks/patron.rb +56 -58
- data/lib/aikido/zen/sinks/pg.rb +23 -25
- data/lib/aikido/zen/sinks/resolv.rb +21 -21
- data/lib/aikido/zen/sinks/socket.rb +10 -12
- data/lib/aikido/zen/sinks/sqlite3.rb +18 -21
- data/lib/aikido/zen/sinks/trilogy.rb +10 -12
- data/lib/aikido/zen/sinks.rb +1 -4
- data/lib/aikido/zen/sinks_dsl.rb +39 -15
- data/lib/aikido/zen/version.rb +1 -1
- data/lib/aikido/zen.rb +7 -2
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 465d5decea5ac5abf60172f2db49977bea1e3325451e594c22890e7f7dabf06a
|
4
|
+
data.tar.gz: 4ae92c15b6b57ca6ea4c7050c49667c8a5bc63349982ec354491ea06c76f4c91
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 967c7e53e134f863289a9be77903ead79998bdbcf4abcfdd60d2ed06b9a79e7e98a158648a0043a616a3a27101820d6a49698f5c32f904eb80cc376d75fc9228
|
7
|
+
data.tar.gz: 95a5a5fb3531e3212cbc285d362d7179c35abb818671764115db449c2d39cfb8b9b652ed5d89c186b7482a311a4db3b233a43b9abcacd0312ca15f61fb163351
|
data/docs/rails.md
CHANGED
@@ -2,19 +2,63 @@
|
|
2
2
|
|
3
3
|
To install Zen, add the gem:
|
4
4
|
|
5
|
-
```
|
5
|
+
```sh
|
6
6
|
bundle add aikido-zen
|
7
7
|
```
|
8
8
|
|
9
|
+
And require it before `Bundler.require` in `config/application.rb`:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
# config/application.rb
|
13
|
+
require_relative "boot"
|
14
|
+
|
15
|
+
require "rails/all"
|
16
|
+
|
17
|
+
require "aikido-zen"
|
18
|
+
Aikido::Zen.protect!
|
19
|
+
|
20
|
+
# Require the gems listed in Gemfile, including any gems
|
21
|
+
# you've limited to :test, :development, or :production.
|
22
|
+
Bundler.require(*Rails.groups)
|
23
|
+
|
24
|
+
...
|
25
|
+
```
|
26
|
+
|
9
27
|
That's it! Zen will start to run inside your app when it starts getting
|
10
28
|
requests.
|
11
29
|
|
30
|
+
## Rate limiting and user blocking
|
31
|
+
|
32
|
+
If you want to add the rate limiting feature to your app, modify your code like this:
|
33
|
+
|
34
|
+
```ruby
|
35
|
+
# app/controllers/application_controller.rb
|
36
|
+
class ApplicationController < ActionController::Base
|
37
|
+
private
|
38
|
+
|
39
|
+
def current_user
|
40
|
+
return unless session[:user_id]
|
41
|
+
User.find(session[:user_id])
|
42
|
+
end
|
43
|
+
|
44
|
+
def authenticate_user!
|
45
|
+
# Your authentication logic here
|
46
|
+
# ...
|
47
|
+
# Optional, if you want to use user based rate limiting or block specific users
|
48
|
+
Aikido::Zen.set_user(
|
49
|
+
id: current_user.id,
|
50
|
+
name: current_user.name
|
51
|
+
)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
```
|
55
|
+
|
12
56
|
## Configuration
|
13
57
|
|
14
58
|
Zen exposes its configuration object to the Rails configuration, which you can
|
15
59
|
modify in an initializer if desired:
|
16
60
|
|
17
|
-
```
|
61
|
+
```ruby
|
18
62
|
# config/initializers/zen.rb
|
19
63
|
Rails.application.config.zen.api_timeouts = 20
|
20
64
|
```
|
@@ -30,7 +74,7 @@ If you're using Rails' [encrypted credentials][creds], and prefer not storing
|
|
30
74
|
sensitive values in your env vars, you can easily configure Zen for it. For
|
31
75
|
example, assuming the following credentials structure:
|
32
76
|
|
33
|
-
```
|
77
|
+
```yaml
|
34
78
|
# config/credentials.yml.enc
|
35
79
|
zen:
|
36
80
|
token: "AIKIDO_RUNTIME_..."
|
@@ -38,7 +82,7 @@ zen:
|
|
38
82
|
|
39
83
|
You can just tell Zen to use it like so:
|
40
84
|
|
41
|
-
```
|
85
|
+
```ruby
|
42
86
|
# config/initializers/zen.rb
|
43
87
|
Rails.application.config.zen.token = Rails.application.credentials.zen.token
|
44
88
|
```
|
@@ -61,7 +105,7 @@ way.
|
|
61
105
|
By default, Zen will use the Rails logger, prefixing messages with `[aikido]`.
|
62
106
|
You can redirect the log to a separate stream by overriding the logger:
|
63
107
|
|
64
|
-
```
|
108
|
+
```ruby
|
65
109
|
# config/initializers/zen.rb
|
66
110
|
Rails.application.config.zen.logger = Logger.new(...)
|
67
111
|
```
|
data/lib/aikido/zen/config.rb
CHANGED
@@ -64,7 +64,7 @@ module Aikido::Zen
|
|
64
64
|
# @return [string] Path of the socket where the detached agent will listen.
|
65
65
|
# By default, is stored under the root application path with file name
|
66
66
|
# `aikido-detached-agent.sock`
|
67
|
-
|
67
|
+
attr_accessor :detached_agent_socket_path
|
68
68
|
|
69
69
|
# @return [Boolean] is the agent in debugging mode?
|
70
70
|
attr_accessor :debugging
|
@@ -222,9 +222,8 @@ module Aikido::Zen
|
|
222
222
|
@api_timeouts.update(value)
|
223
223
|
end
|
224
224
|
|
225
|
-
def
|
226
|
-
@detached_agent_socket_path
|
227
|
-
@detached_agent_socket_path = "drbunix:" + @detached_agent_socket_path unless @detached_agent_socket_path.start_with?("drbunix:")
|
225
|
+
def detached_agent_socket_uri
|
226
|
+
"drbunix:" + @detached_agent_socket_path
|
228
227
|
end
|
229
228
|
|
230
229
|
private
|
@@ -32,7 +32,7 @@ module Aikido::Zen::DetachedAgent
|
|
32
32
|
@polling_interval = polling_interval
|
33
33
|
@worker = worker
|
34
34
|
@collector = collector
|
35
|
-
@detached_agent_front = DRbObject.new_with_uri(config.
|
35
|
+
@detached_agent_front = DRbObject.new_with_uri(config.detached_agent_socket_uri)
|
36
36
|
@has_forked = false
|
37
37
|
schedule_tasks
|
38
38
|
end
|
@@ -1,41 +1,78 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "fileutils"
|
4
|
+
|
3
5
|
module Aikido::Zen::DetachedAgent
|
4
6
|
class Server
|
7
|
+
# Initialize and start a detached agent server instance.
|
8
|
+
#
|
9
|
+
# @return [Aikido::Zen::DetachedAgent::Server]
|
10
|
+
def self.start(**opts)
|
11
|
+
new(**opts).tap(&:start!)
|
12
|
+
end
|
13
|
+
|
5
14
|
def initialize(config: Aikido::Zen.config)
|
6
|
-
@
|
7
|
-
@drb_server = DRb.start_service(config.detached_agent_socket_path, @detached_agent_front)
|
15
|
+
@started_at = nil
|
8
16
|
|
9
|
-
|
10
|
-
@drb_server.verbose = config.logger.debug?
|
11
|
-
end
|
17
|
+
@config = config
|
12
18
|
|
13
|
-
|
14
|
-
@
|
19
|
+
@socket_path = config.detached_agent_socket_path
|
20
|
+
@socket_uri = config.detached_agent_socket_uri
|
15
21
|
end
|
16
22
|
|
17
|
-
def
|
18
|
-
|
19
|
-
DRb.stop_service
|
23
|
+
def started?
|
24
|
+
!!@started_at
|
20
25
|
end
|
21
26
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
27
|
+
def start!
|
28
|
+
@config.logger.info("Starting DRb Server...")
|
29
|
+
|
30
|
+
# Try to ensure that the DRb service can start if the DRb service did
|
31
|
+
# not stop cleanly.
|
32
|
+
begin
|
33
|
+
# Check whether the Unix domain socket is in use by another process.
|
34
|
+
UNIXSocket.new(@socket_path).close
|
35
|
+
rescue Errno::ECONNREFUSED
|
36
|
+
@config.logger.debug("Removing residual Unix domain socket...")
|
37
|
+
|
38
|
+
# Remove the residual Unix domain socket.
|
39
|
+
FileUtils.rm_f(@socket_path)
|
40
|
+
rescue
|
41
|
+
# empty
|
42
|
+
end
|
43
|
+
|
44
|
+
@front = FrontObject.new
|
45
|
+
|
46
|
+
# If the Unix domain socket is in use by another process and/or the
|
47
|
+
# residual Unix domain socket could not be removed DRb will raise an
|
48
|
+
# appropriate error.
|
49
|
+
@drb_server = DRb.start_service(@socket_uri, @front)
|
50
|
+
|
51
|
+
# Only show DRb output in debug mode.
|
52
|
+
@drb_server.verbose = @config.logger.debug?
|
53
|
+
|
54
|
+
# Ensure that the DRb server is alive.
|
55
|
+
max_attempts = 10
|
56
|
+
attempts = 0
|
57
|
+
until @drb_server.alive?
|
58
|
+
@config.logger.info("DRb Server still not alive. #{max_attempts - attempts} attempts remaining")
|
59
|
+
sleep 0.1
|
60
|
+
attempts += 1
|
61
|
+
raise Aikido::Zen::DetachedAgentError.new("Impossible to start the dRB server (socket=#{Aikido::Zen.config.detached_agent_socket_path})") \
|
62
|
+
if attempts == max_attempts
|
38
63
|
end
|
64
|
+
|
65
|
+
@started_at = Time.now.utc
|
66
|
+
|
67
|
+
at_exit { stop! if started? }
|
68
|
+
end
|
69
|
+
|
70
|
+
def stop!
|
71
|
+
@config.logger.info("Stopping DRb Server...")
|
72
|
+
@started_at = nil
|
73
|
+
|
74
|
+
@drb_server.stop_service if @drb_server.alive?
|
75
|
+
DRb.stop_service
|
39
76
|
end
|
40
77
|
end
|
41
78
|
end
|
@@ -7,14 +7,6 @@ module Aikido::Zen
|
|
7
7
|
module Sinks
|
8
8
|
module Async
|
9
9
|
module HTTP
|
10
|
-
def self.load_sinks!
|
11
|
-
if Aikido::Zen.satisfy "async-http", ">= 0.70.0"
|
12
|
-
require "async/http"
|
13
|
-
|
14
|
-
::Async::HTTP::Client.prepend(Async::HTTP::ClientExtensions)
|
15
|
-
end
|
16
|
-
end
|
17
|
-
|
18
10
|
SINK = Sinks.add("async-http", scanners: [
|
19
11
|
Scanners::SSRFScanner,
|
20
12
|
OutboundConnectionMonitor
|
@@ -30,48 +22,54 @@ module Aikido::Zen
|
|
30
22
|
end
|
31
23
|
end
|
32
24
|
|
33
|
-
|
34
|
-
|
25
|
+
def self.load_sinks!
|
26
|
+
if Aikido::Zen.satisfy "async-http", ">= 0.70.0"
|
27
|
+
require "async/http"
|
35
28
|
|
36
|
-
|
37
|
-
|
38
|
-
scheme: request.scheme || scheme,
|
39
|
-
authority: request.authority || authority,
|
40
|
-
path: request.path
|
41
|
-
}))
|
29
|
+
::Async::HTTP::Client.class_eval do
|
30
|
+
extend Sinks::DSL
|
42
31
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
32
|
+
sink_around :call do |original_call, request|
|
33
|
+
uri = URI(format("%<scheme>s://%<authority>s%<path>s", {
|
34
|
+
scheme: request.scheme || scheme,
|
35
|
+
authority: request.authority || authority,
|
36
|
+
path: request.path
|
37
|
+
}))
|
49
38
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
39
|
+
wrapped_request = Scanners::SSRFScanner::Request.new(
|
40
|
+
verb: request.method,
|
41
|
+
uri: uri,
|
42
|
+
headers: request.headers.to_h,
|
43
|
+
header_normalizer: ->(value) { Array(value).join(", ") }
|
44
|
+
)
|
56
45
|
|
57
|
-
|
46
|
+
# Store the request information so the DNS sinks can pick it up.
|
47
|
+
context = Aikido::Zen.current_context
|
48
|
+
if context
|
49
|
+
prev_request = context["ssrf.request"]
|
50
|
+
context["ssrf.request"] = wrapped_request
|
51
|
+
end
|
58
52
|
|
59
|
-
|
53
|
+
connection = OutboundConnection.from_uri(uri)
|
60
54
|
|
61
|
-
|
55
|
+
Helpers.scan(wrapped_request, connection, "request")
|
62
56
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
57
|
+
response = original_call.call
|
58
|
+
|
59
|
+
Scanners::SSRFScanner.track_redirects(
|
60
|
+
request: wrapped_request,
|
61
|
+
response: Scanners::SSRFScanner::Response.new(
|
62
|
+
status: response.status,
|
63
|
+
headers: response.headers.to_h,
|
64
|
+
header_normalizer: ->(value) { Array(value).join(", ") }
|
65
|
+
)
|
66
|
+
)
|
71
67
|
|
72
|
-
|
73
|
-
|
74
|
-
|
68
|
+
response
|
69
|
+
ensure
|
70
|
+
context["ssrf.request"] = prev_request if context
|
71
|
+
end
|
72
|
+
end
|
75
73
|
end
|
76
74
|
end
|
77
75
|
end
|
@@ -6,14 +6,6 @@ require_relative "../outbound_connection_monitor"
|
|
6
6
|
module Aikido::Zen
|
7
7
|
module Sinks
|
8
8
|
module Curl
|
9
|
-
def self.load_sinks!
|
10
|
-
if Aikido::Zen.satisfy "curb", ">= 0.2.3"
|
11
|
-
require "curb"
|
12
|
-
|
13
|
-
::Curl::Easy.prepend(Curl::EasyExtensions)
|
14
|
-
end
|
15
|
-
end
|
16
|
-
|
17
9
|
SINK = Sinks.add("curb", scanners: [
|
18
10
|
Scanners::SSRFScanner,
|
19
11
|
OutboundConnectionMonitor
|
@@ -53,59 +45,65 @@ module Aikido::Zen
|
|
53
45
|
end
|
54
46
|
end
|
55
47
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
sink_around :perform do |super_call|
|
60
|
-
wrapped_request = Helpers.wrap_request(self)
|
61
|
-
|
62
|
-
# Store the request information so the DNS sinks can pick it up.
|
63
|
-
context = Aikido::Zen.current_context
|
64
|
-
if context
|
65
|
-
prev_request = context["ssrf.request"]
|
66
|
-
context["ssrf.request"] = wrapped_request
|
67
|
-
end
|
68
|
-
|
69
|
-
connection = OutboundConnection.from_uri(URI(url))
|
70
|
-
|
71
|
-
Helpers.scan(wrapped_request, connection, "request")
|
72
|
-
|
73
|
-
response = super_call.call
|
74
|
-
|
75
|
-
Scanners::SSRFScanner.track_redirects(
|
76
|
-
request: wrapped_request,
|
77
|
-
response: Helpers.wrap_response(self)
|
78
|
-
)
|
48
|
+
def self.load_sinks!
|
49
|
+
if Aikido::Zen.satisfy "curb", ">= 0.2.3"
|
50
|
+
require "curb"
|
79
51
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
52
|
+
::Curl::Easy.class_eval do
|
53
|
+
extend Sinks::DSL
|
54
|
+
|
55
|
+
sink_around :perform do |original_call|
|
56
|
+
wrapped_request = Helpers.wrap_request(self)
|
57
|
+
|
58
|
+
# Store the request information so the DNS sinks can pick it up.
|
59
|
+
context = Aikido::Zen.current_context
|
60
|
+
if context
|
61
|
+
prev_request = context["ssrf.request"]
|
62
|
+
context["ssrf.request"] = wrapped_request
|
63
|
+
end
|
64
|
+
|
65
|
+
connection = OutboundConnection.from_uri(URI(url))
|
66
|
+
|
67
|
+
Helpers.scan(wrapped_request, connection, "request")
|
68
|
+
|
69
|
+
response = original_call.call
|
70
|
+
|
71
|
+
Scanners::SSRFScanner.track_redirects(
|
72
|
+
request: wrapped_request,
|
73
|
+
response: Helpers.wrap_response(self)
|
74
|
+
)
|
75
|
+
|
76
|
+
# When libcurl has follow_location set, it will handle redirections
|
77
|
+
# internally, and expose the "last_effective_url" as the URI that was
|
78
|
+
# last requested in the redirect chain.
|
79
|
+
#
|
80
|
+
# In this case, we can't actually stop the request from happening, but
|
81
|
+
# we can scan again (now that we know another request happened), to
|
82
|
+
# stop the response from being exposed to the user. This downgrades
|
83
|
+
# the SSRF into a blind SSRF, which is better than doing nothing.
|
84
|
+
if url != last_effective_url
|
85
|
+
last_effective_request = Helpers.wrap_request(self, url: last_effective_url)
|
86
|
+
|
87
|
+
# Code coverage is disabled here because the else clause is a no-op,
|
88
|
+
# so there is nothing to cover.
|
89
|
+
# :nocov:
|
90
|
+
if context
|
91
|
+
context["ssrf.request"] = last_effective_request
|
92
|
+
else
|
93
|
+
# empty
|
94
|
+
end
|
95
|
+
# :nocov:
|
96
|
+
|
97
|
+
connection = OutboundConnection.from_uri(URI(last_effective_url))
|
98
|
+
|
99
|
+
Helpers.scan(last_effective_request, connection, "request")
|
100
|
+
end
|
101
|
+
|
102
|
+
response
|
103
|
+
ensure
|
104
|
+
context["ssrf.request"] = prev_request if context
|
98
105
|
end
|
99
|
-
# :nocov:
|
100
|
-
|
101
|
-
connection = OutboundConnection.from_uri(URI(last_effective_url))
|
102
|
-
|
103
|
-
Helpers.scan(last_effective_request, connection, "request")
|
104
106
|
end
|
105
|
-
|
106
|
-
response
|
107
|
-
ensure
|
108
|
-
context["ssrf.request"] = prev_request if context
|
109
107
|
end
|
110
108
|
end
|
111
109
|
end
|
@@ -7,19 +7,6 @@ module Aikido::Zen
|
|
7
7
|
module Sinks
|
8
8
|
module EventMachine
|
9
9
|
module HttpRequest
|
10
|
-
def self.load_sinks!
|
11
|
-
if Aikido::Zen.satisfy "em-http-request", ">= 1.0"
|
12
|
-
require "em-http-request"
|
13
|
-
|
14
|
-
::EventMachine::HttpRequest.use(EventMachine::HttpRequest::Middleware)
|
15
|
-
|
16
|
-
# NOTE: We can't use middleware to intercept requests as we want to ensure any
|
17
|
-
# modifications to the request from user-supplied middleware are already applied
|
18
|
-
# before we scan the request.
|
19
|
-
::EventMachine::HttpClient.prepend(EventMachine::HttpRequest::HttpClientExtensions)
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
10
|
SINK = Sinks.add("em-http-request", scanners: [
|
24
11
|
Scanners::SSRFScanner,
|
25
12
|
OutboundConnectionMonitor
|
@@ -35,26 +22,37 @@ module Aikido::Zen
|
|
35
22
|
end
|
36
23
|
end
|
37
24
|
|
38
|
-
|
39
|
-
|
25
|
+
def self.load_sinks!
|
26
|
+
if Aikido::Zen.satisfy "em-http-request", ">= 1.0"
|
27
|
+
require "em-http-request"
|
40
28
|
|
41
|
-
|
42
|
-
wrapped_request = Scanners::SSRFScanner::Request.new(
|
43
|
-
verb: req.method.to_s,
|
44
|
-
uri: URI(req.uri),
|
45
|
-
headers: req.headers
|
46
|
-
)
|
29
|
+
::EventMachine::HttpRequest.use(EventMachine::HttpRequest::Middleware)
|
47
30
|
|
48
|
-
#
|
49
|
-
|
50
|
-
|
31
|
+
# NOTE: We can't use middleware to intercept requests as we want to ensure any
|
32
|
+
# modifications to the request from user-supplied middleware are already applied
|
33
|
+
# before we scan the request.
|
34
|
+
::EventMachine::HttpClient.class_eval do
|
35
|
+
extend Sinks::DSL
|
51
36
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
37
|
+
sink_before :send_request do
|
38
|
+
wrapped_request = Scanners::SSRFScanner::Request.new(
|
39
|
+
verb: req.method.to_s,
|
40
|
+
uri: URI(req.uri),
|
41
|
+
headers: req.headers
|
42
|
+
)
|
43
|
+
|
44
|
+
# Store the request information so the DNS sinks can pick it up.
|
45
|
+
context = Aikido::Zen.current_context
|
46
|
+
context["ssrf.request"] = wrapped_request if context
|
47
|
+
|
48
|
+
connection = OutboundConnection.new(
|
49
|
+
host: req.host,
|
50
|
+
port: req.port
|
51
|
+
)
|
56
52
|
|
57
|
-
|
53
|
+
Helpers.scan(wrapped_request, connection, "request")
|
54
|
+
end
|
55
|
+
end
|
58
56
|
end
|
59
57
|
end
|
60
58
|
|