rack_attack_admin 0.1.1 → 0.1.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.
- checksums.yaml +4 -4
- data/Changelog.md +11 -0
- data/Readme.md +93 -10
- data/Screenshot-not_find_allow2ban_rule.png +0 -0
- data/Screenshot.png +0 -0
- data/app/controllers/rack_attack_admin/application_controller.rb +15 -1
- data/app/controllers/rack_attack_admin/rack_attack_controller.rb +6 -2
- data/app/views/rack_attack_admin/banned_ips/_banned_ip.html.haml +4 -2
- data/app/views/rack_attack_admin/rack_attack/current_request.html.haml +10 -0
- data/app/views/rack_attack_admin/rack_attack/index.html.haml +44 -50
- data/lib/rack/attack_extensions.rb +60 -6
- data/lib/rack_attack_admin/version.rb +1 -1
- metadata +6 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 56573b4e1953af195a40c77e4ff7a43dc0092ab28752985ff3f21d92ae4b27ce
|
4
|
+
data.tar.gz: 2c224cb40e27bbabac363ba02ed2b37259595d1c4342be2ab2884167d85cb5c3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6fd53b0f1e13e09370292b1e3121c1135096d0f4e4f211d11be87dcf588fa4e3a0fadcf8b5f2fab4f666d1600ebf35566022f3d5430833da801d76cde1816ae0
|
7
|
+
data.tar.gz: ca371334c028fcf6e1eb632ca0f8371e58e9cd02c3cf1126e522a430babc30cc953c5e67606d67ee0f73b25f167e7887d005f351cfdc892f9cf41bf8249524b0
|
data/Changelog.md
CHANGED
@@ -0,0 +1,11 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
## 0.1.2 (2019-06-17)
|
4
|
+
|
5
|
+
- Various fixes and improvements to the dashboard templates
|
6
|
+
- Make it work also when Rack::Attack.cache.store is set to an instance of ActiveSupport::Cache::RedisCacheStore (from rails)
|
7
|
+
- Let current_request respond to .html as well as .json
|
8
|
+
|
9
|
+
## 0.1.1 (2019-04-02)
|
10
|
+
|
11
|
+
Initial release
|
data/Readme.md
CHANGED
@@ -1,4 +1,8 @@
|
|
1
|
-
# Rack::Attack Admin Dashboard
|
1
|
+
# [Rack::Attack](https://github.com/kickstarter/rack-attack) Admin Dashboard [](https://badge.fury.io/rb/rack_attack_admin)
|
2
|
+
|
3
|
+
Lets you see the current state of all throttles and bans. Delete existing keys/bans. Manually add bans.
|
4
|
+
|
5
|
+

|
2
6
|
|
3
7
|
Inspired by: https://www.backerkit.com/blog/building-a-rackattack-dashboard/
|
4
8
|
|
@@ -10,11 +14,6 @@ Add this line to your application's `Gemfile`:
|
|
10
14
|
gem 'rack_attack_admin'
|
11
15
|
```
|
12
16
|
|
13
|
-
And then execute:
|
14
|
-
|
15
|
-
$ bundle
|
16
|
-
|
17
|
-
|
18
17
|
Add this line to your application's `config/routes.rb`:
|
19
18
|
|
20
19
|
```ruby
|
@@ -23,13 +22,97 @@ mount RackAttackAdmin::Engine, at: '/admin/rack_attack'
|
|
23
22
|
|
24
23
|
## Usage
|
25
24
|
|
26
|
-
|
25
|
+
Go to http://localhost:3000/admin/rack_attack in your browser!
|
26
|
+
|
27
|
+
You can also use the provided read-only command-line command, `rake rack_attack_admin:watch` instead of the web interface:
|
28
|
+
```
|
29
|
+
+-------------------------------+--------------------+
|
30
|
+
| Banned IP | Previous (5 s ago) |
|
31
|
+
+-------------------------------+--------------------+
|
32
|
+
| allow2ban:ban:login-127.0.0.1 | |
|
33
|
+
+-------------------------------+--------------------+
|
34
|
+
+----------------------------------------------------------------------------------+---------------+--------------------+
|
35
|
+
| Key | Current Count | Previous (5 s ago) |
|
36
|
+
+----------------------------------------------------------------------------------+---------------+--------------------+
|
37
|
+
| ('allow2ban:count'):login-127.0.0.1 | 2 | 1 |
|
38
|
+
| throttle('logins/ip'):127.0.0.1 | 1 | |
|
39
|
+
| throttle('logins/email'):me@example.com | 1 | |
|
40
|
+
| throttle('req/ip'):127.0.0.1 | 2 | 1 |
|
41
|
+
+----------------------------------------------------------------------------------+---------------+--------------------+
|
42
|
+
```
|
43
|
+
|
27
44
|
|
28
|
-
##
|
45
|
+
## Fail2Ban/Allow2Ban
|
29
46
|
|
30
|
-
|
47
|
+
In order to allow your Fail2Ban/Allow2Ban rules to be introspected by this app, you must define them
|
48
|
+
slightly differently than the upstream [Rack::Attack
|
49
|
+
documentation](https://github.com/kickstarter/rack-attack#fail2ban) tells you
|
50
|
+
to define them:
|
51
|
+
|
52
|
+
If you have an Allow2Ban filter in a blocklist like this:
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
blocklist('login:allow2ban') do |req|
|
56
|
+
Rack::Attack::Allow2Ban.filter("login-#{req.ip}", maxretry: 5, findtime: 1.minute, bantime: 10.minutes) do
|
57
|
+
# The count for the IP is incremented if this return value is truthy.
|
58
|
+
is_login.(req)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
```
|
62
|
+
|
63
|
+
, you can change it to this equivalent definition:
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
blocklist('login:allow2ban') do |req|
|
67
|
+
def_allow2ban('login', limit: 5, period: 1.minute, bantime: 10.minutes)
|
68
|
+
allow2ban( 'login', req.ip) do
|
69
|
+
is_login.(req)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
```
|
31
73
|
|
32
|
-
|
74
|
+
`def_fail2ban`/`def_allow2ban` save your configuration in a hash (by name, _without_ discriminator in it), much the same as
|
75
|
+
`throttle` does.
|
76
|
+
|
77
|
+
`fail2ban`/`allow2ban` methods are simply wrappers for `Rack::Attack::Allow2Ban.filter` that look up and use the options from the definition (that matches the given name).
|
78
|
+
|
79
|
+
This has the following advantages:
|
80
|
+
- It actually stores your Fail2Ban rule so that we can look it up later
|
81
|
+
- It provides an API and options that are more similar to and consistent with regular `throttle`
|
82
|
+
commands:
|
83
|
+
```ruby
|
84
|
+
# Compare:
|
85
|
+
def_allow2ban('login', limit: 5, period: 1.minute, bantime: …)
|
86
|
+
allow2ban('login', discriminator) do
|
87
|
+
# Return truthy value to increment counter
|
88
|
+
end
|
89
|
+
# allow2ban returns true if counter reaches limit
|
90
|
+
|
91
|
+
throttle('logins/email', limit: 5, period: 1.minute) do |req|
|
92
|
+
discriminator.(req)
|
93
|
+
end
|
94
|
+
```
|
95
|
+
- You can use the familiar `limit` and `period` options instead of `maxretry` and `findtime`
|
96
|
+
options, respectively.
|
97
|
+
- You don't have to interpolate the discriminator into a string like you do with the standard
|
98
|
+
`Fail2Ban.filter` syntax.
|
99
|
+
|
100
|
+
|
101
|
+
This is completely optional. If you choose not to define them this way, it will still show your
|
102
|
+
fail2ban counter keys and value; it just won't be able to find the matching rule, and therefore
|
103
|
+
won't be able to show what the limit or time bucket is for that counter key. So instead of showing
|
104
|
+
the limit for `allow2ban('login')` like it shows in the screenshot above, it will fall back to
|
105
|
+
just showing what little it can show about that key:
|
106
|
+
|
107
|
+

|
108
|
+
|
109
|
+
## Redis cache store compatibility
|
110
|
+
|
111
|
+
This has been tested with `Rack::Attack.cache.store` set to an instance of:
|
112
|
+
- `Redis::Store` from the fantastic [redis-store](https://github.com/redis-store/redis-store) gem.
|
113
|
+
(Which is used by the `ActiveSupport::Cache::RedisStore` (from redis-activesupport/redis-rails
|
114
|
+
gems))
|
115
|
+
- `ActiveSupport::Cache::RedisCacheStore` (provided by Rails 5.2+)
|
33
116
|
|
34
117
|
## Contributing
|
35
118
|
|
Binary file
|
data/Screenshot.png
ADDED
Binary file
|
@@ -13,7 +13,21 @@ module RackAttackAdmin
|
|
13
13
|
|
14
14
|
helper_method \
|
15
15
|
def is_redis?
|
16
|
-
Rack::Attack.cache.respond.store.
|
16
|
+
Rack::Attack.cache.respond.store.to_s.match?(/Redis/)
|
17
|
+
end
|
18
|
+
|
19
|
+
helper_method \
|
20
|
+
def redis
|
21
|
+
return unless is_redis?
|
22
|
+
store = Rack::Attack.cache.store
|
23
|
+
store = store.redis if store.respond_to?(:redis)
|
24
|
+
store = store.data if store.respond_to?(:data)
|
25
|
+
store
|
26
|
+
end
|
27
|
+
|
28
|
+
helper_method \
|
29
|
+
def has_ttl?
|
30
|
+
!!redis
|
17
31
|
end
|
18
32
|
|
19
33
|
helper_method \
|
@@ -5,14 +5,18 @@ module RackAttackAdmin
|
|
5
5
|
# Web version of lib/tasks/rack_attack_admin_tasks.rake
|
6
6
|
def index
|
7
7
|
@default_banned_ip = Rack::Attack::BannedIp.new(bantime: '60 m')
|
8
|
-
@banned_ip_keys = Rack::Attack::Fail2Ban.banned_ip_keys
|
9
8
|
@counters_h = Rack::Attack.counters_h.
|
10
9
|
without(*Rack::Attack::BannedIps.keys)
|
11
10
|
render
|
12
11
|
end
|
13
12
|
|
14
13
|
def current_request
|
15
|
-
|
14
|
+
respond_to do |format|
|
15
|
+
format.json do
|
16
|
+
render json: current_request_rack_attack_stats
|
17
|
+
end
|
18
|
+
format.html
|
19
|
+
end
|
16
20
|
end
|
17
21
|
end
|
18
22
|
end
|
@@ -1,3 +1,4 @@
|
|
1
|
+
- expires_column = local_assigns.key?(:expires_column) ? local_assigns[:expires_column] : has_ttl?
|
1
2
|
%tr
|
2
3
|
%td= key
|
3
4
|
|
@@ -6,9 +7,10 @@
|
|
6
7
|
= ip
|
7
8
|
(#{link_to 'Look up', "http://ip-lookup.net/?ip=#{ip}", target:'_blank'})
|
8
9
|
|
9
|
-
- if
|
10
|
+
- if expires_column
|
10
11
|
%td
|
11
|
-
-
|
12
|
+
- if has_ttl?
|
13
|
+
- interval = redis.ttl("#{Rack::Attack.prefix_with_namespace}:#{key}")
|
12
14
|
- if interval
|
13
15
|
=# distance_of_time_in_words(interval)
|
14
16
|
in #{ActiveSupport::Duration.build(interval)&.human_str}
|
@@ -4,14 +4,14 @@
|
|
4
4
|
%tr
|
5
5
|
%th Key
|
6
6
|
%th Banned IP
|
7
|
-
|
8
|
-
%th Expires
|
7
|
+
%th Expires
|
9
8
|
%th Actions
|
10
9
|
|
11
10
|
= render partial: 'rack_attack_admin/banned_ips/banned_ip',
|
12
11
|
collection: Rack::Attack::BannedIps.keys, as: 'key',
|
13
12
|
locals: { _:nil,
|
14
13
|
full_key_prefix: Rack::Attack::BannedIps.full_key_prefix,
|
14
|
+
expires_column: true,
|
15
15
|
}
|
16
16
|
|
17
17
|
= form_for @default_banned_ip, url: [rack_attack_admin, :banned_ips] do |f|
|
@@ -27,18 +27,17 @@
|
|
27
27
|
%tr
|
28
28
|
%th Key
|
29
29
|
%th Banned IP
|
30
|
-
- if
|
30
|
+
- if has_ttl?
|
31
31
|
%th Expires
|
32
32
|
%th Actions
|
33
33
|
|
34
|
-
|
35
|
-
|
36
|
-
collection: @banned_ip_keys, as: 'key',
|
34
|
+
= render partial: 'rack_attack_admin/banned_ips/banned_ip',
|
35
|
+
collection: Rack::Attack::Fail2Ban.banned_ip_keys, as: 'key',
|
37
36
|
locals: { _:nil,
|
38
37
|
full_key_prefix: Rack::Attack::Fail2Ban.full_key_prefix,
|
39
38
|
}
|
40
39
|
|
41
|
-
%h1 Throttle/Fail2Ban
|
40
|
+
%h1 Throttle/Fail2Ban Counters
|
42
41
|
%table.table.table-striped.mb-0
|
43
42
|
%thead
|
44
43
|
%tr
|
@@ -52,11 +51,25 @@
|
|
52
51
|
(Time bucket)
|
53
52
|
%th Actions
|
54
53
|
- @counters_h.each do |key, value|
|
54
|
+
:ruby
|
55
|
+
parsed = Rack::Attack.parse_key(key)
|
56
|
+
value = value.to_i
|
57
|
+
name = Rack::Attack.humanize_key(key).sub(":#{parsed[:discriminator]}", '')
|
58
|
+
|
59
|
+
# We can get expires_in from redis or directly from the matched throttle rule
|
60
|
+
interval =
|
61
|
+
if has_ttl?
|
62
|
+
redis.ttl("#{Rack::Attack.prefix_with_namespace}:#{key}")
|
63
|
+
elsif parsed and time_range = parsed[:time_range]
|
64
|
+
(time_range.end - Time.now)
|
65
|
+
end
|
66
|
+
if parsed
|
67
|
+
time_range = parsed[:time_range]
|
68
|
+
end
|
69
|
+
if time_range
|
70
|
+
human_duration = time_range.duration&.human_str
|
71
|
+
end
|
55
72
|
%tr
|
56
|
-
:ruby
|
57
|
-
parsed = Rack::Attack.parse_key(key)
|
58
|
-
value = value.to_i
|
59
|
-
name = Rack::Attack.humanize_key(key).sub(":#{parsed[:discriminator]}", '')
|
60
73
|
%td
|
61
74
|
= name
|
62
75
|
-# if parsed and rule = parsed[:rule]
|
@@ -65,52 +78,33 @@
|
|
65
78
|
%td= parsed[:discriminator]
|
66
79
|
|
67
80
|
- limit = parsed && (rule = parsed[:rule]) && rule.limit.to_i
|
68
|
-
- over_limit = value >= limit
|
81
|
+
- over_limit = limit && value >= limit
|
69
82
|
%td{class: ('over_limit' if over_limit), style: ('color: red' if over_limit)}
|
70
83
|
= value
|
84
|
+
<br/>
|
71
85
|
- if limit
|
72
|
-
|
86
|
+
Limit:
|
87
|
+
= "#{limit}"
|
88
|
+
= "/#{human_duration}" if human_duration
|
89
|
+
<br/>
|
90
|
+
- if time_range
|
91
|
+
%small
|
92
|
+
(#{(limit / (time_range.duration/60.0)).round(0)}/min)
|
73
93
|
|
74
94
|
%td
|
75
|
-
-# We can get expires_in from redis or directly from the mached throttle rule
|
76
|
-
:ruby
|
77
|
-
interval =
|
78
|
-
if is_redis?
|
79
|
-
Rack::Attack.cache.store.ttl("#{Rack::Attack.cache.prefix}:#{key}")
|
80
|
-
elsif parsed and time_range = parsed[:time_range]
|
81
|
-
(time_range.end - Time.now)
|
82
|
-
end
|
83
95
|
- if interval
|
84
|
-
|
96
|
+
- if interval < 0
|
97
|
+
expired<br/>
|
98
|
+
- elsif interval
|
99
|
+
in #{ActiveSupport::Duration.build(interval)&.human_str}<br/>
|
85
100
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
101
|
+
- if time_range
|
102
|
+
%small
|
103
|
+
(#{ time_range.begin.to_s(:time_with_s)}
|
104
|
+
%span><
|
105
|
+
\-
|
106
|
+
#{time_range.end .to_s(:time_with_s)}
|
107
|
+
\= #{human_duration})
|
93
108
|
|
94
109
|
%td= link_to 'Delete', rack_attack_admin.key_path(key), method: :delete, class: 'btn'
|
95
110
|
.current_time.mb-2 (Current time: #{Time.now.to_s(:time_with_s)})
|
96
|
-
|
97
|
-
%h1 Flags
|
98
|
-
%table.table.table-striped
|
99
|
-
%thead
|
100
|
-
%tr
|
101
|
-
%th Key
|
102
|
-
%th Value
|
103
|
-
%tr
|
104
|
-
%td cookies[:skip_safelist]
|
105
|
-
%td= cookies[:skip_safelist]
|
106
|
-
|
107
|
-
%h1 Current Request
|
108
|
-
%table.table.table-striped
|
109
|
-
%thead
|
110
|
-
%tr
|
111
|
-
%th Key
|
112
|
-
%th Value
|
113
|
-
- current_request_rack_attack_stats.each do |key, value|
|
114
|
-
%tr
|
115
|
-
%td= key
|
116
|
-
%td= value
|
@@ -6,8 +6,65 @@ class Rack::Attack
|
|
6
6
|
class << self
|
7
7
|
extend Memoist
|
8
8
|
|
9
|
+
def all_keys
|
10
|
+
store, namespace = cache_store_and_namespace_to_strip
|
11
|
+
keys = store.keys
|
12
|
+
if namespace
|
13
|
+
keys.map {|key| key.to_s.sub(/^#{namespace}:/, '') }
|
14
|
+
else
|
15
|
+
keys
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# The same as cache.prefix but prefixed with "{namespace}:" if namespace option is set and needs
|
20
|
+
# to be stripped from keys returned from store.keys.
|
21
|
+
# Like cache.prefix, this does not include the trailing ':'.
|
22
|
+
def prefix_with_namespace_to_strip
|
23
|
+
prefix = cache.prefix
|
24
|
+
store, namespace = cache_store_and_namespace_to_strip
|
25
|
+
if namespace
|
26
|
+
prefix = "#{namespace}:#{prefix}"
|
27
|
+
end
|
28
|
+
prefix
|
29
|
+
end
|
30
|
+
|
31
|
+
# The same as cache.prefix but prefixed with "{namespace}:" if namespace option is set.
|
32
|
+
# Needed when passing a key directly to a Redis command, like Redis#ttl, since Redis class
|
33
|
+
# doesn't know about namespacing.
|
34
|
+
def prefix_with_namespace
|
35
|
+
prefix = cache.prefix
|
36
|
+
if namespace = cache_namespace
|
37
|
+
prefix = "#{namespace}:#{prefix}"
|
38
|
+
end
|
39
|
+
prefix
|
40
|
+
end
|
41
|
+
|
42
|
+
def cache_namespace
|
43
|
+
cache.store&.options&.[](:namespace)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Returns an array of [cache_store, namespace_to_strip]
|
47
|
+
# This will either be [a Redis::Store, nil] or
|
48
|
+
# [a Redis , namespace_to_strip]
|
49
|
+
def cache_store_and_namespace_to_strip
|
50
|
+
store = cache.store
|
51
|
+
# Store can be a ActiveSupport::Cache::RedisCacheStore, a Redis::Store object, or a Redis object.
|
52
|
+
# If it is a ActiveSupport::Cache::RedisCacheStore, then we need to get the redis object in
|
53
|
+
# order to get keys from it.
|
54
|
+
store = store.redis if store.respond_to?(:redis)
|
55
|
+
if store.respond_to?(:data) # Redis::Store already stripped namespaced
|
56
|
+
store = store.data
|
57
|
+
[store, nil]
|
58
|
+
else
|
59
|
+
# Redis object (which is all we have available in the case of a
|
60
|
+
# ActiveSupport::Cache::RedisCacheStore) unfortunately returns keys with namespace prefix in
|
61
|
+
# each key, so we need to strip this out (Redis::Store does this already; see store.data.keys above)
|
62
|
+
[store, cache_namespace]
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
9
66
|
def prefixed_keys
|
10
|
-
|
67
|
+
all_keys.grep(/^#{cache.prefix}:/)
|
11
68
|
end
|
12
69
|
|
13
70
|
# AKA unprefixed_keys
|
@@ -56,9 +113,6 @@ class Rack::Attack
|
|
56
113
|
)
|
57
114
|
end
|
58
115
|
|
59
|
-
class ParsedKey
|
60
|
-
end
|
61
|
-
|
62
116
|
# Reverse Cache#key_and_expiry:
|
63
117
|
# … "#{prefix}:#{(@last_epoch_time / period).to_i}:#{unprefixed_key}" …
|
64
118
|
# @return [Hash]
|
@@ -231,7 +285,7 @@ class Rack::Attack
|
|
231
285
|
|
232
286
|
class << self
|
233
287
|
def prefixed_keys
|
234
|
-
|
288
|
+
Rack::Attack.all_keys.grep(/^#{cache.prefix}:(allow|fail)2ban:/)
|
235
289
|
end
|
236
290
|
|
237
291
|
# AKA unprefixed_keys
|
@@ -273,7 +327,7 @@ class Rack::Attack
|
|
273
327
|
class BannedIps
|
274
328
|
class << self
|
275
329
|
def prefixed_keys
|
276
|
-
|
330
|
+
Rack::Attack.all_keys.grep(/^#{full_key_prefix}:/)
|
277
331
|
end
|
278
332
|
|
279
333
|
# Removes only the Rack::Attack.cache.prefix
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rack_attack_admin
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tyler Rick
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-06-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -164,11 +164,14 @@ files:
|
|
164
164
|
- License
|
165
165
|
- Rakefile
|
166
166
|
- Readme.md
|
167
|
+
- Screenshot-not_find_allow2ban_rule.png
|
168
|
+
- Screenshot.png
|
167
169
|
- app/controllers/rack_attack_admin/application_controller.rb
|
168
170
|
- app/controllers/rack_attack_admin/banned_ips_controller.rb
|
169
171
|
- app/controllers/rack_attack_admin/keys_controller.rb
|
170
172
|
- app/controllers/rack_attack_admin/rack_attack_controller.rb
|
171
173
|
- app/views/rack_attack_admin/banned_ips/_banned_ip.html.haml
|
174
|
+
- app/views/rack_attack_admin/rack_attack/current_request.html.haml
|
172
175
|
- app/views/rack_attack_admin/rack_attack/index.html.haml
|
173
176
|
- bin/console
|
174
177
|
- bin/setup
|
@@ -201,7 +204,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
201
204
|
- !ruby/object:Gem::Version
|
202
205
|
version: '0'
|
203
206
|
requirements: []
|
204
|
-
rubygems_version: 3.0.
|
207
|
+
rubygems_version: 3.0.3
|
205
208
|
signing_key:
|
206
209
|
specification_version: 4
|
207
210
|
summary: A Rack::Attack admin dashboard
|