parry 0.1.0 → 0.2.0
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 +10 -2
- data/README.md +320 -18
- data/Rakefile +3 -1
- data/app/controllers/parry/application_controller.rb +8 -0
- data/app/controllers/parry/blocked_hosts_controller.rb +85 -0
- data/app/controllers/parry/rules_controller.rb +109 -0
- data/app/helpers/parry/application_helper.rb +21 -0
- data/app/views/layouts/parry/application.html.erb +341 -0
- data/app/views/parry/blocked_hosts/index.html.erb +88 -0
- data/app/views/parry/rules/_form.html.erb +101 -0
- data/app/views/parry/rules/edit.html.erb +10 -0
- data/app/views/parry/rules/import.html.erb +56 -0
- data/app/views/parry/rules/index.html.erb +80 -0
- data/app/views/parry/rules/new.html.erb +10 -0
- data/config/routes.rb +19 -0
- data/lib/parry/blocked_host.rb +53 -0
- data/lib/parry/caught_host.rb +33 -0
- data/lib/parry/configuration.rb +49 -0
- data/lib/parry/engine.rb +17 -0
- data/lib/parry/honeypot_store.rb +72 -0
- data/lib/parry/rack_attack_adapter.rb +212 -0
- data/lib/parry/rule.rb +202 -0
- data/lib/parry/rule_set.rb +56 -0
- data/lib/parry/rule_store.rb +88 -0
- data/lib/parry/rule_transfer.rb +118 -0
- data/lib/parry/tracker.rb +89 -0
- data/lib/parry/version.rb +1 -1
- data/lib/parry.rb +84 -1
- metadata +113 -4
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Parry
|
|
4
|
+
class RuleSet
|
|
5
|
+
attr_reader :version, :rules, :caught_hosts
|
|
6
|
+
|
|
7
|
+
def initialize(rules = [], version = 0, caught_hosts = {})
|
|
8
|
+
@rules = rules.freeze
|
|
9
|
+
@version = version
|
|
10
|
+
@caught_hosts = caught_hosts.freeze
|
|
11
|
+
@blocklist_rules = rules.select { |rule| rule.kind == "blocklist" }.freeze
|
|
12
|
+
@safelist_rules = rules.select { |rule| rule.kind == "safelist" }.freeze
|
|
13
|
+
@throttle_rules = rules.select(&:throttle?).freeze
|
|
14
|
+
@honeypot_rules = rules.select(&:honeypot?).freeze
|
|
15
|
+
freeze
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.empty
|
|
19
|
+
new([], 0, {})
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
attr_reader :blocklist_rules, :safelist_rules, :throttle_rules, :honeypot_rules
|
|
23
|
+
|
|
24
|
+
def blocklisted?(ip)
|
|
25
|
+
!blocklist_rule_for(ip).nil?
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def safelisted?(ip)
|
|
29
|
+
!safelist_rule_for(ip).nil?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def blocklist_rule_for(ip)
|
|
33
|
+
@blocklist_rules.find { |rule| rule.matches_ip?(ip) }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def safelist_rule_for(ip)
|
|
37
|
+
@safelist_rules.find { |rule| rule.matches_ip?(ip) }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def honeypot_rule_for_path(path)
|
|
41
|
+
@honeypot_rules.find { |rule| rule.matches_path?(path) }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def caught?(ip)
|
|
45
|
+
!caught_host(ip).nil?
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def caught_host(ip)
|
|
49
|
+
@caught_hosts[ip.to_s]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def empty?
|
|
53
|
+
@rules.empty? && @caught_hosts.empty?
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module Parry
|
|
6
|
+
class RuleStore
|
|
7
|
+
def all
|
|
8
|
+
raw = Parry.with_redis { |redis| redis.hgetall(rules_key) }
|
|
9
|
+
raw.values.filter_map { |json| Rule.from_json(json) }.sort_by { |rule| [rule.created_at || 0, rule.id.to_s] }
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def where(kind:)
|
|
13
|
+
all.select { |rule| rule.kind == kind.to_s }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def find(id)
|
|
17
|
+
json = Parry.with_redis { |redis| redis.hget(rules_key, id.to_s) }
|
|
18
|
+
json && Rule.from_json(json)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def count
|
|
22
|
+
Parry.with_redis { |redis| redis.hlen(rules_key) }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def save(rule)
|
|
26
|
+
rule.id ||= SecureRandom.hex(8)
|
|
27
|
+
rule.normalize!
|
|
28
|
+
return false unless rule.valid?
|
|
29
|
+
|
|
30
|
+
rule.created_at ||= Time.now.to_i
|
|
31
|
+
Parry.with_redis do |redis|
|
|
32
|
+
redis.hset(rules_key, rule.id, rule.to_json)
|
|
33
|
+
redis.incr(version_key)
|
|
34
|
+
end
|
|
35
|
+
rule.persisted = true
|
|
36
|
+
true
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def delete(id)
|
|
40
|
+
deleted = Parry.with_redis do |redis|
|
|
41
|
+
count = redis.hdel(rules_key, id.to_s)
|
|
42
|
+
redis.incr(version_key) if count > 0
|
|
43
|
+
count
|
|
44
|
+
end
|
|
45
|
+
deleted > 0
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def delete_matching_ip(ip, kinds: ["blocklist"])
|
|
49
|
+
kinds = Array(kinds).map(&:to_s)
|
|
50
|
+
matching = all.select { |rule| kinds.include?(rule.kind) && rule.matches_ip?(ip) }
|
|
51
|
+
return matching if matching.empty?
|
|
52
|
+
|
|
53
|
+
Parry.with_redis do |redis|
|
|
54
|
+
redis.hdel(rules_key, matching.map(&:id))
|
|
55
|
+
redis.incr(version_key)
|
|
56
|
+
end
|
|
57
|
+
matching
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def version
|
|
61
|
+
Parry.with_redis { |redis| redis.get(version_key) }.to_i
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def bump_version
|
|
65
|
+
Parry.with_redis { |redis| redis.incr(version_key) }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def rule_set
|
|
69
|
+
current = version
|
|
70
|
+
RuleSet.new(all, current, Parry.honeypots.to_h)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def clear
|
|
74
|
+
Parry.with_redis do |redis|
|
|
75
|
+
redis.del(rules_key)
|
|
76
|
+
redis.incr(version_key)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def rules_key
|
|
81
|
+
"#{Parry.config.key_prefix}:rules"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def version_key
|
|
85
|
+
"#{Parry.config.key_prefix}:rules_version"
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Parry
|
|
6
|
+
class RuleTransfer
|
|
7
|
+
FORMAT = 1
|
|
8
|
+
MAX_BYTES = 1_048_576
|
|
9
|
+
|
|
10
|
+
Result = Struct.new(:imported, :errors, :replaced, keyword_init: true) do
|
|
11
|
+
def ok?
|
|
12
|
+
errors.empty?
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def replaced?
|
|
16
|
+
!!replaced
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class << self
|
|
21
|
+
def export
|
|
22
|
+
{
|
|
23
|
+
"format" => FORMAT,
|
|
24
|
+
"parry_version" => Parry::VERSION,
|
|
25
|
+
"exported_at" => Time.now.to_i,
|
|
26
|
+
"rules" => Parry.store.all.map(&:to_h)
|
|
27
|
+
}
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def export_json
|
|
31
|
+
JSON.pretty_generate(export)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def filename(at: Time.now)
|
|
35
|
+
"parry-rules-#{at.strftime("%Y%m%d-%H%M%S")}.json"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def import(payload, replace: false)
|
|
39
|
+
payload = payload.to_s
|
|
40
|
+
return failure("The file is empty.") if payload.strip.empty?
|
|
41
|
+
return failure("The file is bigger than #{MAX_BYTES / 1024}KB.") if payload.bytesize > MAX_BYTES
|
|
42
|
+
|
|
43
|
+
data = begin
|
|
44
|
+
JSON.parse(payload)
|
|
45
|
+
rescue JSON::ParserError => e
|
|
46
|
+
return failure("The file is not valid JSON (#{e.message.lines.first.to_s.strip}).")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
entries = entries_in(data)
|
|
50
|
+
return failure(%(No rules found. Expected an export with a "rules" list.)) if entries.nil?
|
|
51
|
+
return failure("The file contains no rules.") if entries.empty?
|
|
52
|
+
|
|
53
|
+
apply(entries, replace: replace)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def entries_in(data)
|
|
59
|
+
case data
|
|
60
|
+
when Hash then data["rules"].is_a?(Array) ? data["rules"] : nil
|
|
61
|
+
when Array then data
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def apply(entries, replace:)
|
|
66
|
+
snapshot = Parry.with_redis { |redis| redis.hgetall(Parry.store.rules_key) }
|
|
67
|
+
errors = []
|
|
68
|
+
imported = 0
|
|
69
|
+
|
|
70
|
+
Parry.store.clear if replace
|
|
71
|
+
|
|
72
|
+
entries.each_with_index do |attrs, index|
|
|
73
|
+
rule = build(attrs)
|
|
74
|
+
|
|
75
|
+
if rule.nil?
|
|
76
|
+
errors << "Rule #{index + 1} could not be read."
|
|
77
|
+
elsif Parry.store.save(rule)
|
|
78
|
+
imported += 1
|
|
79
|
+
else
|
|
80
|
+
errors << "Rule #{index + 1}#{label_for(rule)}: #{rule.errors.full_messages.to_sentence}."
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
if errors.any?
|
|
85
|
+
restore(snapshot)
|
|
86
|
+
return Result.new(imported: 0, errors: errors, replaced: false)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
Result.new(imported: imported, errors: [], replaced: replace)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def build(attrs)
|
|
93
|
+
return nil unless attrs.is_a?(Hash)
|
|
94
|
+
|
|
95
|
+
rule = Rule.from_json(attrs.to_json)
|
|
96
|
+
rule&.persisted = false
|
|
97
|
+
rule
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def label_for(rule)
|
|
101
|
+
label = rule.label
|
|
102
|
+
label.present? ? " (#{label})" : ""
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def restore(snapshot)
|
|
106
|
+
Parry.with_redis do |redis|
|
|
107
|
+
redis.del(Parry.store.rules_key)
|
|
108
|
+
snapshot.each { |id, json| redis.hset(Parry.store.rules_key, id, json) }
|
|
109
|
+
redis.incr(Parry.store.version_key)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def failure(message)
|
|
114
|
+
Result.new(imported: 0, errors: [message], replaced: false)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Parry
|
|
4
|
+
class Tracker
|
|
5
|
+
def record(ip:, match_type:, rule: nil, path: nil, at: Time.now)
|
|
6
|
+
return if ip.nil? || ip.to_s.empty?
|
|
7
|
+
|
|
8
|
+
timestamp = at.to_i
|
|
9
|
+
key = host_key(ip)
|
|
10
|
+
Parry.with_redis do |redis|
|
|
11
|
+
redis.multi do |tx|
|
|
12
|
+
tx.hincrby(key, "hits", 1)
|
|
13
|
+
tx.hset(key, "ip", ip.to_s, "match_type", match_type.to_s, "last_seen", timestamp.to_s)
|
|
14
|
+
tx.hsetnx(key, "first_seen", timestamp.to_s)
|
|
15
|
+
tx.hset(key, "rule", rule.to_s) if rule
|
|
16
|
+
tx.hset(key, "path", path.to_s) if path
|
|
17
|
+
tx.zadd(index_key, timestamp, ip.to_s)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
trim!
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def all(limit: 100)
|
|
24
|
+
ips = Parry.with_redis { |redis| redis.zrevrange(index_key, 0, limit - 1) }
|
|
25
|
+
return [] if ips.empty?
|
|
26
|
+
|
|
27
|
+
hashes = Parry.with_redis do |redis|
|
|
28
|
+
redis.pipelined { |pipeline| ips.each { |ip| pipeline.hgetall(host_key(ip)) } }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
ips.zip(hashes).filter_map { |ip, hash| BlockedHost.from_hash(ip, hash) }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def find(ip)
|
|
35
|
+
hash = Parry.with_redis { |redis| redis.hgetall(host_key(ip)) }
|
|
36
|
+
BlockedHost.from_hash(ip, hash)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def count
|
|
40
|
+
Parry.with_redis { |redis| redis.zcard(index_key) }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def forget(ip)
|
|
44
|
+
Parry.with_redis do |redis|
|
|
45
|
+
redis.multi do |tx|
|
|
46
|
+
tx.zrem(index_key, ip.to_s)
|
|
47
|
+
tx.del(host_key(ip))
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
true
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def clear
|
|
54
|
+
Parry.with_redis do |redis|
|
|
55
|
+
ips = redis.zrange(index_key, 0, -1)
|
|
56
|
+
redis.del(*ips.map { |ip| host_key(ip) }) unless ips.empty?
|
|
57
|
+
redis.del(index_key)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def index_key
|
|
62
|
+
"#{Parry.config.key_prefix}:blocked_hosts"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def host_key(ip)
|
|
66
|
+
"#{Parry.config.key_prefix}:blocked_host:#{ip}"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def trim!
|
|
72
|
+
max = Parry.config.max_tracked_hosts
|
|
73
|
+
return if max.nil? || max <= 0
|
|
74
|
+
|
|
75
|
+
Parry.with_redis do |redis|
|
|
76
|
+
excess = redis.zcard(index_key) - max
|
|
77
|
+
next if excess <= 0
|
|
78
|
+
|
|
79
|
+
victims = redis.zrange(index_key, 0, excess - 1)
|
|
80
|
+
next if victims.empty?
|
|
81
|
+
|
|
82
|
+
redis.multi do |tx|
|
|
83
|
+
tx.zrem(index_key, victims)
|
|
84
|
+
tx.del(*victims.map { |ip| host_key(ip) })
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
data/lib/parry/version.rb
CHANGED
data/lib/parry.rb
CHANGED
|
@@ -1,8 +1,91 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "logger"
|
|
4
|
+
require "active_support"
|
|
5
|
+
require "active_support/core_ext/object/blank"
|
|
6
|
+
require "active_support/core_ext/hash/keys"
|
|
7
|
+
require "active_support/core_ext/hash/slice"
|
|
8
|
+
require "rack/attack"
|
|
9
|
+
|
|
3
10
|
require_relative "parry/version"
|
|
11
|
+
require_relative "parry/configuration"
|
|
12
|
+
require_relative "parry/rule"
|
|
13
|
+
require_relative "parry/rule_set"
|
|
14
|
+
require_relative "parry/rule_store"
|
|
15
|
+
require_relative "parry/rule_transfer"
|
|
16
|
+
require_relative "parry/blocked_host"
|
|
17
|
+
require_relative "parry/caught_host"
|
|
18
|
+
require_relative "parry/honeypot_store"
|
|
19
|
+
require_relative "parry/tracker"
|
|
20
|
+
require_relative "parry/rack_attack_adapter"
|
|
4
21
|
|
|
5
22
|
module Parry
|
|
6
23
|
class Error < StandardError; end
|
|
7
|
-
|
|
24
|
+
|
|
25
|
+
class << self
|
|
26
|
+
attr_writer :logger
|
|
27
|
+
|
|
28
|
+
def config
|
|
29
|
+
@config ||= Configuration.new
|
|
30
|
+
end
|
|
31
|
+
alias_method :configuration, :config
|
|
32
|
+
|
|
33
|
+
def configure
|
|
34
|
+
yield config
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def with_redis(&block)
|
|
38
|
+
client = config.redis
|
|
39
|
+
client.respond_to?(:with) ? client.with(&block) : yield(client)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def store
|
|
43
|
+
@store ||= RuleStore.new
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def tracker
|
|
47
|
+
@tracker ||= Tracker.new
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def honeypots
|
|
51
|
+
@honeypots ||= HoneypotStore.new
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def adapter
|
|
55
|
+
@adapter ||= RackAttackAdapter.new
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def install!
|
|
59
|
+
adapter.install!
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def rules
|
|
63
|
+
store.all
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def unblock(ip)
|
|
67
|
+
adapter.unblock(ip)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def blocked_hosts(limit: 100)
|
|
71
|
+
tracker.all(limit: limit)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def logger
|
|
75
|
+
@logger ||= if defined?(::Rails) && ::Rails.respond_to?(:logger) && ::Rails.logger
|
|
76
|
+
::Rails.logger
|
|
77
|
+
else
|
|
78
|
+
::Logger.new($stderr)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def reset!
|
|
83
|
+
@store = nil
|
|
84
|
+
@tracker = nil
|
|
85
|
+
@honeypots = nil
|
|
86
|
+
adapter.reset!
|
|
87
|
+
end
|
|
88
|
+
end
|
|
8
89
|
end
|
|
90
|
+
|
|
91
|
+
require_relative "parry/engine" if defined?(::Rails)
|
metadata
CHANGED
|
@@ -1,15 +1,101 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: parry
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Greg Molnar
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
-
dependencies:
|
|
12
|
-
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: railties
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '7.1'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '7.1'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: actionpack
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '7.1'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '7.1'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: activemodel
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '7.1'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '7.1'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: activesupport
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '7.1'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '7.1'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: rack-attack
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - ">="
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '6.6'
|
|
75
|
+
type: :runtime
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - ">="
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '6.6'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: redis
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - ">="
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '4.2'
|
|
89
|
+
type: :runtime
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - ">="
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '4.2'
|
|
96
|
+
description: Parry is a mountable Rails engine to manage Rack::Attack rules (blocklists,
|
|
97
|
+
safelists and throttles) at runtime, inspect blocked hosts and unblock them. Rules
|
|
98
|
+
are stored in Redis and applied without a redeploy.
|
|
13
99
|
email:
|
|
14
100
|
- molnargerg@gmail.com
|
|
15
101
|
executables: []
|
|
@@ -21,7 +107,30 @@ files:
|
|
|
21
107
|
- LICENSE.txt
|
|
22
108
|
- README.md
|
|
23
109
|
- Rakefile
|
|
110
|
+
- app/controllers/parry/application_controller.rb
|
|
111
|
+
- app/controllers/parry/blocked_hosts_controller.rb
|
|
112
|
+
- app/controllers/parry/rules_controller.rb
|
|
113
|
+
- app/helpers/parry/application_helper.rb
|
|
114
|
+
- app/views/layouts/parry/application.html.erb
|
|
115
|
+
- app/views/parry/blocked_hosts/index.html.erb
|
|
116
|
+
- app/views/parry/rules/_form.html.erb
|
|
117
|
+
- app/views/parry/rules/edit.html.erb
|
|
118
|
+
- app/views/parry/rules/import.html.erb
|
|
119
|
+
- app/views/parry/rules/index.html.erb
|
|
120
|
+
- app/views/parry/rules/new.html.erb
|
|
121
|
+
- config/routes.rb
|
|
24
122
|
- lib/parry.rb
|
|
123
|
+
- lib/parry/blocked_host.rb
|
|
124
|
+
- lib/parry/caught_host.rb
|
|
125
|
+
- lib/parry/configuration.rb
|
|
126
|
+
- lib/parry/engine.rb
|
|
127
|
+
- lib/parry/honeypot_store.rb
|
|
128
|
+
- lib/parry/rack_attack_adapter.rb
|
|
129
|
+
- lib/parry/rule.rb
|
|
130
|
+
- lib/parry/rule_set.rb
|
|
131
|
+
- lib/parry/rule_store.rb
|
|
132
|
+
- lib/parry/rule_transfer.rb
|
|
133
|
+
- lib/parry/tracker.rb
|
|
25
134
|
- lib/parry/version.rb
|
|
26
135
|
- sig/parry.rbs
|
|
27
136
|
homepage: https://github.com/gregmolnar/parry
|
|
@@ -46,5 +155,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
46
155
|
requirements: []
|
|
47
156
|
rubygems_version: 4.0.3
|
|
48
157
|
specification_version: 4
|
|
49
|
-
summary:
|
|
158
|
+
summary: A Rails engine that provides a GUI for Rack::Attack
|
|
50
159
|
test_files: []
|