safer_redis 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module SaferRedis
6
+ class CommandDoc
7
+ CATEGORY_SLOW = "@slow"
8
+ CATEGORY_DANGEROUS = "@dangerous"
9
+
10
+ # JSON-parsed Redis command data from the embedded copy of
11
+ # https://github.com/redis/redis-doc/blob/master/commands.json
12
+ def self.commands_data
13
+ @commands_data ||= begin
14
+ base_dir = File.absolute_path(File.join(__dir__, "..", ".."))
15
+ path = File.join(base_dir, "data", "redis-doc", "commands.json")
16
+
17
+ JSON.parse(File.read(path))
18
+ end
19
+ end
20
+
21
+ # The `redis` gem represents commands internally as an array with the command name
22
+ # as a lower-case symbol as the first item, e.g. [:del, "foo", "bar"]
23
+ def self.from_command_array(a)
24
+ new(a.first.to_s.upcase)
25
+ end
26
+
27
+ def initialize(name)
28
+ @name = name
29
+ end
30
+
31
+ attr_reader :name
32
+
33
+ def url
34
+ slug = name.downcase.gsub(" ", "-")
35
+
36
+ "https://redis.io/commands/#{slug}/"
37
+ end
38
+
39
+ def slow?
40
+ acl_categories.include?(CATEGORY_SLOW)
41
+ end
42
+
43
+ def dangerous?
44
+ acl_categories.include?(CATEGORY_DANGEROUS)
45
+ end
46
+
47
+ def acl_categories
48
+ command.fetch("acl_categories", [])
49
+ end
50
+
51
+ def complexity
52
+ command.fetch("complexity", nil)
53
+ end
54
+
55
+ private
56
+
57
+ def command
58
+ @command ||= (self.class.commands_data[name] || {})
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SaferRedis
4
+ class Danger < Error
5
+ def initialize(doc)
6
+ message = <<~MESSAGE
7
+ The #{doc.name} Redis command might be dangerous.
8
+
9
+ #{doc.url}
10
+
11
+ ACL categories: #{doc.acl_categories.join(" ")}
12
+
13
+ Complexity: #{doc.complexity}
14
+
15
+ If you're sure this is okay, you can try again within `SaferRedis.really { ... }`
16
+ MESSAGE
17
+
18
+ super(message)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SaferRedis
4
+ module Interceptor
5
+ def send_command(command, &block)
6
+ if SaferRedis.active?
7
+ SaferRedis.assess!(SaferRedis::CommandDoc.from_command_array(command))
8
+ end
9
+
10
+ super
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SaferRedis
4
+ VERSION = "1.0.0"
5
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zeitwerk"
4
+ Zeitwerk::Loader.for_gem.setup
5
+
6
+ module SaferRedis
7
+ class Error < StandardError; end
8
+
9
+ def self.activate!(klass: Redis)
10
+ klass.prepend(SaferRedis::Interceptor)
11
+ @active = true
12
+ end
13
+
14
+ def self.deactivate!
15
+ @active = false
16
+ end
17
+
18
+ def self.active?
19
+ defined?(@active) ? @active : false
20
+ end
21
+
22
+ def self.really
23
+ was = active?
24
+ @active = false
25
+ yield
26
+ ensure
27
+ @active = was
28
+ end
29
+
30
+ def self.assess!(doc)
31
+ if doc.dangerous? || doc.slow?
32
+ raise SaferRedis::Danger.new(doc)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/safer_redis/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "safer_redis"
7
+ spec.version = SaferRedis::VERSION
8
+ spec.authors = ["Paul Annesley"]
9
+ spec.email = ["paul@annesley.cc"]
10
+
11
+ spec.summary = "Catch unsafe Redis commands in production"
12
+ spec.description = "SaferRedis warns you before letting through commands that could impact production availability by being marked `@slow` or `@dangerous` in the Redis documentation"
13
+ spec.homepage = "https://github.com/buildkite/safer_redis"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = ">= 2.6.0"
16
+
17
+ spec.metadata["homepage_uri"] = spec.homepage
18
+ spec.metadata["source_code_uri"] = "https://github.com/buildkite/safer_redis"
19
+ spec.metadata["changelog_uri"] = "https://github.com/buildkite/safer_redis/blob/main/CHANGELOG.md"
20
+
21
+ # Specify which files should be added to the gem when it is released.
22
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
23
+ spec.files = Dir.chdir(__dir__) do
24
+ `git ls-files -z`.split("\x0").reject do |f|
25
+ (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
26
+ end
27
+ end
28
+ spec.require_paths = ["lib"]
29
+
30
+ # The only strategy implemented so far is intercepting the private Redis#send_command method,
31
+ # which was introduced in v4.6.0 via https://github.com/redis/redis-rb/pull/1058 and remains
32
+ # in the latest release (v5.0.5) at time of writing.
33
+ spec.add_dependency "redis", ">= 4.6.0"
34
+ spec.add_dependency "zeitwerk"
35
+ end
@@ -0,0 +1,4 @@
1
+ module SaferRedis
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,91 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: safer_redis
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Paul Annesley
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-12-02 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: redis
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 4.6.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 4.6.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: zeitwerk
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description: SaferRedis warns you before letting through commands that could impact
42
+ production availability by being marked `@slow` or `@dangerous` in the Redis documentation
43
+ email:
44
+ - paul@annesley.cc
45
+ executables: []
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - ".rspec"
50
+ - CHANGELOG.md
51
+ - CODE_OF_CONDUCT.md
52
+ - Gemfile
53
+ - Gemfile.lock
54
+ - LICENSE.txt
55
+ - README.md
56
+ - Rakefile
57
+ - data/redis-doc/commands.json
58
+ - lib/safer_redis.rb
59
+ - lib/safer_redis/command_doc.rb
60
+ - lib/safer_redis/danger.rb
61
+ - lib/safer_redis/interceptor.rb
62
+ - lib/safer_redis/version.rb
63
+ - safer_redis.gemspec
64
+ - sig/safer_redis.rbs
65
+ homepage: https://github.com/buildkite/safer_redis
66
+ licenses:
67
+ - MIT
68
+ metadata:
69
+ homepage_uri: https://github.com/buildkite/safer_redis
70
+ source_code_uri: https://github.com/buildkite/safer_redis
71
+ changelog_uri: https://github.com/buildkite/safer_redis/blob/main/CHANGELOG.md
72
+ post_install_message:
73
+ rdoc_options: []
74
+ require_paths:
75
+ - lib
76
+ required_ruby_version: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ version: 2.6.0
81
+ required_rubygems_version: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ requirements: []
87
+ rubygems_version: 3.1.6
88
+ signing_key:
89
+ specification_version: 4
90
+ summary: Catch unsafe Redis commands in production
91
+ test_files: []