safe_bro 0.1.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.
Files changed (53) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +215 -0
  4. data/Rakefile +3 -0
  5. data/app/controllers/safe_bro/application_controller.rb +50 -0
  6. data/app/controllers/safe_bro/audits_controller.rb +35 -0
  7. data/app/controllers/safe_bro/flagged_commands_controller.rb +14 -0
  8. data/app/controllers/safe_bro/rules_controller.rb +18 -0
  9. data/app/controllers/safe_bro/sessions_controller.rb +67 -0
  10. data/app/controllers/safe_bro/unlocks_controller.rb +14 -0
  11. data/app/helpers/safe_bro/application_helper.rb +59 -0
  12. data/app/jobs/safe_bro/purge_job.rb +10 -0
  13. data/app/models/safe_bro/application_record.rb +6 -0
  14. data/app/models/safe_bro/audit.rb +19 -0
  15. data/app/models/safe_bro/command.rb +20 -0
  16. data/app/models/safe_bro/sensitive_access.rb +13 -0
  17. data/app/models/safe_bro/session.rb +52 -0
  18. data/app/views/safe_bro/flagged_commands/index.html.erb +22 -0
  19. data/app/views/safe_bro/rules/show.html.erb +53 -0
  20. data/app/views/safe_bro/sessions/index.html.erb +51 -0
  21. data/app/views/safe_bro/sessions/show.html.erb +91 -0
  22. data/app/views/safe_bro/shared/forbidden.html.erb +3 -0
  23. data/app/views/safe_bro/unlocks/index.html.erb +25 -0
  24. data/config/routes.rb +11 -0
  25. data/db/migrate/20261114000001_create_safe_bro_tables.rb +52 -0
  26. data/lib/generators/safe_bro/install/install_generator.rb +35 -0
  27. data/lib/generators/safe_bro/install/templates/safe_bro.rb +45 -0
  28. data/lib/safe_bro/cipher.rb +45 -0
  29. data/lib/safe_bro/commands.rb +25 -0
  30. data/lib/safe_bro/configuration.rb +153 -0
  31. data/lib/safe_bro/encrypted_type.rb +16 -0
  32. data/lib/safe_bro/engine.rb +33 -0
  33. data/lib/safe_bro/errors.rb +41 -0
  34. data/lib/safe_bro/executor.rb +100 -0
  35. data/lib/safe_bro/guards/const_guard.rb +39 -0
  36. data/lib/safe_bro/guards/credentials_guard.rb +31 -0
  37. data/lib/safe_bro/guards/env_guard.rb +67 -0
  38. data/lib/safe_bro/guards/file_guard.rb +28 -0
  39. data/lib/safe_bro/guards/guarded.rb +43 -0
  40. data/lib/safe_bro/guards/irb_context.rb +23 -0
  41. data/lib/safe_bro/guards/method_guard.rb +31 -0
  42. data/lib/safe_bro/guards/network_guard.rb +98 -0
  43. data/lib/safe_bro/guards/sql_guard.rb +18 -0
  44. data/lib/safe_bro/parsed_command.rb +66 -0
  45. data/lib/safe_bro/recorder.rb +77 -0
  46. data/lib/safe_bro/shield.rb +83 -0
  47. data/lib/safe_bro/supervisor.rb +68 -0
  48. data/lib/safe_bro/terminal.rb +55 -0
  49. data/lib/safe_bro/validator.rb +74 -0
  50. data/lib/safe_bro/version.rb +3 -0
  51. data/lib/safe_bro.rb +98 -0
  52. data/lib/tasks/safe_bro.rake +7 -0
  53. metadata +141 -0
@@ -0,0 +1,83 @@
1
+ module SafeBro
2
+ # The shield decides what is protected right now. Everything starts locked. unlock! asks for
3
+ # a justification, records it, and opens the requested resources until lock!.
4
+ #
5
+ # Resources:
6
+ # encrypted_data - Active Record encrypted attributes read as ciphertext
7
+ # credentials - Rails credentials, their keys, secret_key_base
8
+ # env - ENV values whose names look secret
9
+ # files - master.key, .env files
10
+ # network - connections to config.protected_urls
11
+ class Shield
12
+ RESOURCES = %i[ encrypted_data credentials env files network ].freeze
13
+
14
+ def initialize
15
+ @unlocked = Set.new
16
+ end
17
+
18
+ def locked?
19
+ @unlocked.empty?
20
+ end
21
+
22
+ def unlocked?(resource)
23
+ @unlocked.include?(:all) || @unlocked.include?(resource)
24
+ end
25
+
26
+ # Is this resource protected from the console right now?
27
+ def protects?(resource)
28
+ SafeBro.config.protected_resources.include?(resource) && !unlocked?(resource)
29
+ end
30
+
31
+ def unlock(resources = [ :all ])
32
+ unknown = resources - RESOURCES - SafeBro.config.protected_resources - [ :all ]
33
+ raise ArgumentError, "Unknown resource #{unknown.first.inspect}. Choose from #{(RESOURCES | SafeBro.config.protected_resources).join(', ')}." if unknown.any?
34
+
35
+ SafeBro.executor.run_as_system do
36
+ justification = Terminal.ask(SafeBro.config.unlock_prompt)
37
+ SafeBro.recorder.start_sensitive_access(resources, justification)
38
+ @unlocked = Set.new(resources)
39
+ Terminal.say(SafeBro.config.unlocked_message, :yellow)
40
+ end
41
+ end
42
+
43
+ def lock(silent: false)
44
+ SafeBro.executor.run_as_system do
45
+ was_unlocked = !locked?
46
+ SafeBro.recorder.end_sensitive_access if SafeBro.recorder.session
47
+ @unlocked = Set.new
48
+ Terminal.say(SafeBro.config.locked_message, :yellow) if was_unlocked && !silent
49
+ end
50
+ end
51
+
52
+ # Runs the block with the protections that work by wrapping execution switched on.
53
+ def protect(&block)
54
+ if protects?(:encrypted_data) && encryption_available?
55
+ ActiveRecord::Encryption.protecting_encrypted_data(&block)
56
+ else
57
+ block.call
58
+ end
59
+ end
60
+
61
+ def install
62
+ ActiveRecord::ConnectionAdapters::AbstractAdapter.prepend(Guards::SqlGuard)
63
+ Guards::NetworkGuard.install if SafeBro.config.protected_urls.any?
64
+ Guards::CredentialsGuard.install
65
+ Guards::EnvGuard.install
66
+ Guards::FileGuard.install
67
+ Guards::ConstGuard.install
68
+ Guards::MethodGuard.install
69
+ install_irb
70
+ end
71
+
72
+ private
73
+ def install_irb
74
+ IRB::Context.prepend(Guards::IrbContext)
75
+ IRB::Command.register(:unlock!, Commands::Unlock)
76
+ IRB::Command.register(:lock!, Commands::Lock)
77
+ end
78
+
79
+ def encryption_available?
80
+ defined?(ActiveRecord::Encryption) && ActiveRecord::Encryption.respond_to?(:protecting_encrypted_data)
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,68 @@
1
+ module SafeBro
2
+ # Entry point for a console (or runner) process: installs the protections once, then
3
+ # identifies the person, asks why, and opens a session.
4
+ class Supervisor
5
+ def run_console
6
+ install
7
+ start
8
+ end
9
+
10
+ def run_runner
11
+ start(source: "runner", reason: runner_reason, username: runner_username, interactive: false)
12
+ SafeBro.recorder.record_command(runner_statements)
13
+ end
14
+
15
+ def install
16
+ return if @installed
17
+
18
+ # Loaded ahead of time: without this, the first command in a real console raised
19
+ # "uninitialized constant ActiveRecord::QueryMethods::Relation" (console1984 hit the same).
20
+ require "active_record/relation"
21
+ SafeBro.shield.install
22
+ SafeBro.lock_down! if SafeBro.config.freeze_internals
23
+ @installed = true
24
+ end
25
+
26
+ def start(source: "console", reason: nil, username: nil, interactive: true)
27
+ SafeBro.shield.lock(silent: true)
28
+ Terminal.say(SafeBro.config.welcome_message, :yellow) if interactive
29
+ username ||= current_username
30
+ reason ||= session_reason(username)
31
+ SafeBro.recorder.start_session(username: username, reason: reason, source: source)
32
+ at_exit { stop }
33
+ end
34
+
35
+ def stop
36
+ SafeBro.recorder.finish_session
37
+ end
38
+
39
+ def current_username
40
+ SafeBro.config.username_resolver.call.to_s.strip.presence ||
41
+ (SafeBro.config.ask_for_username_if_empty && Terminal.interactive? && Terminal.ask("Please, enter your name:")) ||
42
+ raise(MissingUsername)
43
+ end
44
+
45
+ private
46
+ # CONSOLE_REASON answers without a terminal; otherwise ask, and refuse to continue without one.
47
+ def session_reason(username)
48
+ env_reason = ENV["CONSOLE_REASON"].to_s.squish.first(255).presence
49
+ return env_reason if env_reason
50
+ raise MissingReason unless Terminal.interactive?
51
+
52
+ Terminal.ask("#{username}, why are you using this console today?")
53
+ end
54
+
55
+ # Runners are usually automation: don't refuse, but record that nobody said who or why.
56
+ def runner_reason
57
+ ENV["CONSOLE_REASON"].to_s.squish.first(255).presence || "(no reason given)"
58
+ end
59
+
60
+ def runner_username
61
+ SafeBro.config.username_resolver.call.to_s.strip.presence || "(unattributed)"
62
+ end
63
+
64
+ def runner_statements
65
+ "rails runner #{ARGV.join(' ')}".strip
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,55 @@
1
+ module SafeBro
2
+ # Talking to the person at the console.
3
+ module Terminal
4
+ COLORS = { green: 32, yellow: 33, red: 31, blue: 34 }.freeze
5
+
6
+ class << self
7
+ # Tests silence console output.
8
+ attr_accessor :muted
9
+
10
+ def interactive?
11
+ $stdin.tty?
12
+ end
13
+
14
+ def say(message, color = nil)
15
+ return if muted
16
+
17
+ text = "\n#{message}\n"
18
+ $stdout.puts(color && $stdout.tty? ? "\e[#{COLORS.fetch(color)}m#{text}\e[0m" : text)
19
+ end
20
+
21
+ # Something worth the person's attention, on stderr so it doesn't mix with output they pipe.
22
+ def alert(message)
23
+ $stderr.puts("SafeBro: #{message}") unless muted
24
+ end
25
+
26
+ # Asks until it gets a non-blank answer. Raises NotInteractive when there's nobody to ask.
27
+ def ask(question)
28
+ raise NotInteractive unless interactive?
29
+
30
+ say(question, :green)
31
+ answer = nil
32
+ with_plain_output do
33
+ loop do
34
+ line = Reline.readline("> ", false)
35
+ raise NotInteractive, "No answer given." if line.nil?
36
+
37
+ answer = line.strip
38
+ break unless answer.empty?
39
+ end
40
+ end
41
+ answer
42
+ end
43
+
44
+ private
45
+ # IRB colorizes what you type; the answer should look like plain text.
46
+ def with_plain_output
47
+ original = Reline.output_modifier_proc
48
+ Reline.output_modifier_proc = nil
49
+ yield
50
+ ensure
51
+ Reline.output_modifier_proc = original
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,74 @@
1
+ module SafeBro
2
+ # Static analysis of a console command, before it runs. Each validation raises
3
+ # ForbiddenCommand (blocked) or SuspiciousCommand (runs, but is flagged).
4
+ #
5
+ # This is a speed bump for honest mistakes and a tripwire for dishonest attempts. It is not
6
+ # a sandbox: Ruby is too dynamic for that. The runtime guards (see Guards) cover the most
7
+ # obvious ways around it, and the rest is what the audit trail is for.
8
+ class Validator
9
+ class Validation
10
+ def initialize(config)
11
+ @config = config
12
+ end
13
+
14
+ attr_reader :config
15
+ end
16
+
17
+ # class Foo / module Foo for a protected Foo or Foo::Bar.
18
+ class ForbiddenReopening < Validation
19
+ def validate(command, locked:)
20
+ command.reopened_constants.each do |name|
21
+ if config.forbidden_reopening.any? { |forbidden| name == forbidden || name.start_with?("#{forbidden}::") }
22
+ raise ForbiddenCommand, "Reopening #{name} is not allowed."
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ class ForbiddenConstantReference < Validation
29
+ def validate(command, locked:)
30
+ forbidden = config.forbidden_constants.fetch(:always, [])
31
+ forbidden += config.forbidden_constants.fetch(:locked, []) if locked
32
+ command.constant_paths.each do |path|
33
+ match = forbidden.find { |f| path == f || path.start_with?("#{f}::") }
34
+ raise ForbiddenCommand, "Referencing #{match} is not allowed#{' while locked' unless config.forbidden_constants.fetch(:always, []).include?(match)}." if match
35
+ end
36
+ end
37
+ end
38
+
39
+ # Method names from forbidden_methods ("Kernel#eval" -> eval), wherever they appear.
40
+ class ForbiddenMethods < Validation
41
+ def validate(command, locked:)
42
+ names = config.forbidden_methods.map { |entry| entry.split(/[#.]/, 2).last }.uniq
43
+ used = command.method_names & names
44
+ raise ForbiddenCommand, "Calling #{used.first} is not allowed." if used.any?
45
+ end
46
+ end
47
+
48
+ class SuspiciousTerms < Validation
49
+ def validate(command, locked:)
50
+ term = config.suspicious_terms.find { |t| command.source.include?(t) }
51
+ raise SuspiciousCommand, "Command mentions #{term}." if term
52
+ end
53
+ end
54
+
55
+ VALIDATIONS = [ ForbiddenReopening, ForbiddenConstantReference, ForbiddenMethods, SuspiciousTerms ].freeze
56
+
57
+ def initialize(config = SafeBro.config)
58
+ @validations = VALIDATIONS.map { |klass| klass.new(config) }
59
+ end
60
+
61
+ # Raises on the first failing validation. Forbidden beats suspicious, so run every
62
+ # validation and only then raise a suspicion.
63
+ def validate(source, locked: true)
64
+ command = ParsedCommand.new(source)
65
+ suspicion = nil
66
+ @validations.each do |validation|
67
+ validation.validate(command, locked: locked)
68
+ rescue SuspiciousCommand => error
69
+ suspicion ||= error
70
+ end
71
+ raise suspicion if suspicion
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,3 @@
1
+ module SafeBro
2
+ VERSION = "0.1.0"
3
+ end
data/lib/safe_bro.rb ADDED
@@ -0,0 +1,98 @@
1
+ require "irb"
2
+ require "reline"
3
+ require "set"
4
+ require "socket"
5
+
6
+ require "bro_hub"
7
+ require "safe_bro/version"
8
+ require "safe_bro/errors"
9
+ require "safe_bro/configuration"
10
+ require "safe_bro/cipher"
11
+ require "safe_bro/parsed_command"
12
+ require "safe_bro/validator"
13
+ require "safe_bro/terminal"
14
+ require "safe_bro/recorder"
15
+ require "safe_bro/executor"
16
+ require "safe_bro/guards/guarded"
17
+ require "safe_bro/guards/sql_guard"
18
+ require "safe_bro/guards/network_guard"
19
+ require "safe_bro/guards/credentials_guard"
20
+ require "safe_bro/guards/env_guard"
21
+ require "safe_bro/guards/file_guard"
22
+ require "safe_bro/guards/method_guard"
23
+ require "safe_bro/guards/const_guard"
24
+ require "safe_bro/guards/irb_context"
25
+ require "safe_bro/commands"
26
+ require "safe_bro/shield"
27
+ require "safe_bro/supervisor"
28
+ require "safe_bro/engine"
29
+
30
+ # = SafeBro
31
+ #
32
+ # Audited, guarded Rails console access.
33
+ #
34
+ # * Every console session is recorded: who, why, and every command (encrypted at rest).
35
+ # * Sensitive data (encrypted attributes, credentials, ENV secrets, protected systems) is
36
+ # locked until the person runs unlock! and explains why. Everything done while unlocked
37
+ # is linked to that justification.
38
+ # * Commands are checked before they run: forbidden ones are blocked, suspicious ones flagged.
39
+ # * The audit trail itself can't be changed from the console.
40
+ # * Auditors review sessions in a mountable engine.
41
+ #
42
+ # Access from the console is a deterrent plus an audit trail, not a sandbox. See the README.
43
+ module SafeBro
44
+ class << self
45
+ attr_accessor :debug
46
+
47
+ def config
48
+ @config ||= Configuration.new
49
+ end
50
+
51
+ def configure
52
+ yield config
53
+ end
54
+
55
+ def reset_config!
56
+ @config = nil
57
+ end
58
+
59
+ def enabled?
60
+ config.enabled?
61
+ end
62
+
63
+ # The collaborators are created on first use and then shared.
64
+ def supervisor = @supervisor ||= Supervisor.new
65
+ def shield = @shield ||= Shield.new
66
+ def executor = @executor ||= Executor.new
67
+ def recorder = @recorder ||= Recorder.new
68
+ def validator = @validator ||= Validator.new(config)
69
+
70
+ # For gems that expose sensitive things (e.g. a credentials reveal): call this first.
71
+ # Raises ProtectedResource when running under a console command while the resource is
72
+ # locked. With `locations:` (caller_locations) it only raises for calls made directly
73
+ # from what the person typed.
74
+ def guard!(resource, detail = nil, locations: nil)
75
+ return unless executor.executing_user_command? && shield.protects?(resource)
76
+ return if locations && !executor.from_console?(locations)
77
+
78
+ raise ProtectedResource.new(resource, detail)
79
+ end
80
+
81
+ # Subscribe with ActiveSupport::Notifications.subscribe(/\.safe_bro\z/) to alert people.
82
+ # Events: session_started, unlocked, command_flagged, session_flagged.
83
+ def notify(event, **payload)
84
+ ActiveSupport::Notifications.instrument("#{event}.safe_bro", payload)
85
+ rescue => error
86
+ Rails.logger&.error("SafeBro: notification #{event} failed: #{error.class}: #{error.message}")
87
+ end
88
+
89
+ # Freezes configuration and SafeBro's own classes so they can't be monkey patched from a console.
90
+ def lock_down!
91
+ config.lock_down!
92
+ [ Executor, Shield, Recorder, Supervisor, Validator, Validator::Validation, *Validator::VALIDATIONS,
93
+ ParsedCommand, Terminal, Commands::Unlock, Commands::Lock, Guards::Guarded, Guards::SqlGuard,
94
+ Guards::EnvGuard, Guards::FileGuard, Guards::MethodGuard, Guards::ConstGuard, Guards::CredentialsGuard, Guards::CredentialsGuard::Inspect,
95
+ Guards::IrbContext ].each(&:freeze)
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,7 @@
1
+ namespace :safe_bro do
2
+ desc "Delete console sessions older than config.retention (flagged ones are kept unless keep_flagged is off)"
3
+ task purge: :environment do
4
+ deleted = SafeBro::Session.purge!
5
+ puts "SafeBro: purged #{deleted} session#{'s' unless deleted == 1}"
6
+ end
7
+ end
metadata ADDED
@@ -0,0 +1,141 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: safe_bro
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Ruby Dev SRL
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rails
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: irb
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '1.13'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '1.13'
40
+ - !ruby/object:Gem::Dependency
41
+ name: bro_hub
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0.1'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0.1'
54
+ description: Records Rails console sessions (who, why, what), asks for a justification
55
+ before sensitive data is touched, blocks tampering and risky methods, and ships
56
+ a mountable engine where auditors review sessions. One gem instead of console1984
57
+ plus audits1984.
58
+ email:
59
+ - office@rubydev.ro
60
+ executables: []
61
+ extensions: []
62
+ extra_rdoc_files: []
63
+ files:
64
+ - MIT-LICENSE
65
+ - README.md
66
+ - Rakefile
67
+ - app/controllers/safe_bro/application_controller.rb
68
+ - app/controllers/safe_bro/audits_controller.rb
69
+ - app/controllers/safe_bro/flagged_commands_controller.rb
70
+ - app/controllers/safe_bro/rules_controller.rb
71
+ - app/controllers/safe_bro/sessions_controller.rb
72
+ - app/controllers/safe_bro/unlocks_controller.rb
73
+ - app/helpers/safe_bro/application_helper.rb
74
+ - app/jobs/safe_bro/purge_job.rb
75
+ - app/models/safe_bro/application_record.rb
76
+ - app/models/safe_bro/audit.rb
77
+ - app/models/safe_bro/command.rb
78
+ - app/models/safe_bro/sensitive_access.rb
79
+ - app/models/safe_bro/session.rb
80
+ - app/views/safe_bro/flagged_commands/index.html.erb
81
+ - app/views/safe_bro/rules/show.html.erb
82
+ - app/views/safe_bro/sessions/index.html.erb
83
+ - app/views/safe_bro/sessions/show.html.erb
84
+ - app/views/safe_bro/shared/forbidden.html.erb
85
+ - app/views/safe_bro/unlocks/index.html.erb
86
+ - config/routes.rb
87
+ - db/migrate/20261114000001_create_safe_bro_tables.rb
88
+ - lib/generators/safe_bro/install/install_generator.rb
89
+ - lib/generators/safe_bro/install/templates/safe_bro.rb
90
+ - lib/safe_bro.rb
91
+ - lib/safe_bro/cipher.rb
92
+ - lib/safe_bro/commands.rb
93
+ - lib/safe_bro/configuration.rb
94
+ - lib/safe_bro/encrypted_type.rb
95
+ - lib/safe_bro/engine.rb
96
+ - lib/safe_bro/errors.rb
97
+ - lib/safe_bro/executor.rb
98
+ - lib/safe_bro/guards/const_guard.rb
99
+ - lib/safe_bro/guards/credentials_guard.rb
100
+ - lib/safe_bro/guards/env_guard.rb
101
+ - lib/safe_bro/guards/file_guard.rb
102
+ - lib/safe_bro/guards/guarded.rb
103
+ - lib/safe_bro/guards/irb_context.rb
104
+ - lib/safe_bro/guards/method_guard.rb
105
+ - lib/safe_bro/guards/network_guard.rb
106
+ - lib/safe_bro/guards/sql_guard.rb
107
+ - lib/safe_bro/parsed_command.rb
108
+ - lib/safe_bro/recorder.rb
109
+ - lib/safe_bro/shield.rb
110
+ - lib/safe_bro/supervisor.rb
111
+ - lib/safe_bro/terminal.rb
112
+ - lib/safe_bro/validator.rb
113
+ - lib/safe_bro/version.rb
114
+ - lib/tasks/safe_bro.rake
115
+ homepage: https://www.rubydev.ro
116
+ licenses:
117
+ - MIT
118
+ metadata:
119
+ allowed_push_host: https://rubygems.org
120
+ rubygems_mfa_required: 'true'
121
+ homepage_uri: https://www.rubydev.ro
122
+ source_code_uri: https://github.com/rubydevro/safe_bro
123
+ bug_tracker_uri: https://github.com/rubydevro/safe_bro/issues
124
+ rdoc_options: []
125
+ require_paths:
126
+ - lib
127
+ required_ruby_version: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '3.2'
132
+ required_rubygems_version: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - ">="
135
+ - !ruby/object:Gem::Version
136
+ version: '0'
137
+ requirements: []
138
+ rubygems_version: 4.0.10
139
+ specification_version: 4
140
+ summary: Audited, guarded Rails console access, with a built-in review UI.
141
+ test_files: []