console1984 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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +95 -0
- data/Rakefile +32 -0
- data/app/jobs/console1984/incineration_job.rb +17 -0
- data/app/models/console1984/base.rb +7 -0
- data/app/models/console1984/command.rb +14 -0
- data/app/models/console1984/sensitive_access.rb +6 -0
- data/app/models/console1984/session.rb +15 -0
- data/app/models/console1984/session/incineratable.rb +30 -0
- data/app/models/console1984/user.rb +5 -0
- data/config/routes.rb +9 -0
- data/db/migrate/20210517203931_create_console1984_tables.rb +35 -0
- data/lib/console1984.rb +86 -0
- data/lib/console1984/commands.rb +14 -0
- data/lib/console1984/engine.rb +29 -0
- data/lib/console1984/env_variable_username.rb +9 -0
- data/lib/console1984/errors.rb +13 -0
- data/lib/console1984/messages.rb +31 -0
- data/lib/console1984/protected_auditable_tables.rb +26 -0
- data/lib/console1984/protected_context.rb +15 -0
- data/lib/console1984/protected_tcp_socket.rb +56 -0
- data/lib/console1984/sessions_logger/database.rb +57 -0
- data/lib/console1984/supervisor.rb +48 -0
- data/lib/console1984/supervisor/accesses.rb +41 -0
- data/lib/console1984/supervisor/accesses/protected.rb +10 -0
- data/lib/console1984/supervisor/accesses/unprotected.rb +5 -0
- data/lib/console1984/supervisor/executor.rb +41 -0
- data/lib/console1984/supervisor/input_output.rb +11 -0
- data/lib/console1984/username/env_resolver.rb +14 -0
- data/lib/console1984/version.rb +3 -0
- metadata +186 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 985b93be5f065f2f12ec2e121a84d1951aa8d2d0f4e8bf628dccd4680a3581d1
|
4
|
+
data.tar.gz: e5e40eadddc7f44e25e1384c948897058b8fd2ddacafb6f4b2ef3f63e7cc20e8
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b6d2d32d9210d20a082e7844d873e514ba61ec9e18546d4d8e353ed8a1360685c0bcde0406140c635a79b2fb34519148c988e7e5ff1a3579afb97120f5930330
|
7
|
+
data.tar.gz: 7b32ac9d5cd80e4f7840d12c803122ddc521f5256028e03f6f0cd401081c12dee755934393cb005d26ebe5a30a24dfc79b934acb2ee1a6d44dfa9c978137f833
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2020 Jorge Manrubia
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
# Console1984
|
2
|
+
|
3
|
+
A Rails Console that audits commands and protects users privacy.
|
4
|
+
|
5
|
+
> “If you want to keep a secret, you must also hide it from yourself.”
|
6
|
+
>
|
7
|
+
> ― George Orwell, 1984
|
8
|
+
|
9
|
+
## Usage
|
10
|
+
|
11
|
+
Add this line to your application's Gemfile:
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
gem 'console1984'
|
15
|
+
```
|
16
|
+
|
17
|
+
By default, `console1984` will only work in `production`. [You can configure other environments](#protected-environments).
|
18
|
+
|
19
|
+
## Features
|
20
|
+
|
21
|
+
### Auditing
|
22
|
+
|
23
|
+
The console will ask for a reason for the console session, identifying the user via the environment
|
24
|
+
variable `CONSOLE_USER`.
|
25
|
+
|
26
|
+
After that, every command the user types will be captured and logged. `console1984` uses
|
27
|
+
[`rails-structured-logggin`](https://github.com/basecamp/rails-structured-logging) to form
|
28
|
+
a JSON entry that looks like this:
|
29
|
+
|
30
|
+
```json
|
31
|
+
{
|
32
|
+
"@timestamp": "2020-05-15T15:05:45.845642+02:00",
|
33
|
+
"ecs": {
|
34
|
+
"version": "1.2.0"
|
35
|
+
},
|
36
|
+
"event": {
|
37
|
+
"action": "console.audit_trail",
|
38
|
+
"duration": {
|
39
|
+
"ms": 0.01
|
40
|
+
}
|
41
|
+
},
|
42
|
+
"console": {
|
43
|
+
"user": "Jorge",
|
44
|
+
"reason": "fix something",
|
45
|
+
"commands": "Account.first\n"
|
46
|
+
},
|
47
|
+
"rails": {
|
48
|
+
"application": "haystack",
|
49
|
+
"env": "beta"
|
50
|
+
},
|
51
|
+
"ruby": {
|
52
|
+
"allocations": {
|
53
|
+
"count": 0
|
54
|
+
}
|
55
|
+
},
|
56
|
+
"process": {
|
57
|
+
"pid": 8539,
|
58
|
+
"name": "rails_console",
|
59
|
+
"working_directory": "/Users/jorge/Work/basecamp/haystack"
|
60
|
+
},
|
61
|
+
"performance": {
|
62
|
+
"time": {
|
63
|
+
"cpu": {
|
64
|
+
"ms": 0.01
|
65
|
+
},
|
66
|
+
"idle": {
|
67
|
+
"ms": 0.0
|
68
|
+
}
|
69
|
+
}
|
70
|
+
},
|
71
|
+
"original": " Account Load (1.0ms) SELECT `accounts`.* FROM `accounts` ORDER BY `accounts`.`id` ASC LIMIT 1\n"
|
72
|
+
}
|
73
|
+
```
|
74
|
+
|
75
|
+
## Configuration
|
76
|
+
|
77
|
+
### Protected environments
|
78
|
+
|
79
|
+
<a name="protected-environments"></a>
|
80
|
+
|
81
|
+
By default, `console1984` will only be enabled in `production`. You can configure the target environments with
|
82
|
+
`config.console1984.protected_environments`:
|
83
|
+
|
84
|
+
```ruby
|
85
|
+
config.console1984.protected_environments = %i[ staging production ]
|
86
|
+
```
|
87
|
+
|
88
|
+
### Audit logger
|
89
|
+
|
90
|
+
By default, the console will output JSON entries for audit trails to STDOUT. You can configure the
|
91
|
+
used logger with `config.console1984.audit_logger`:
|
92
|
+
|
93
|
+
```ruby
|
94
|
+
config.console1984.audit_logger = ActiveSupport::Logger.new("log/console.txt")
|
95
|
+
```
|
data/Rakefile
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rdoc/task'
|
8
|
+
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
10
|
+
rdoc.rdoc_dir = 'rdoc'
|
11
|
+
rdoc.title = 'Console1984'
|
12
|
+
rdoc.options << '--line-numbers'
|
13
|
+
rdoc.rdoc_files.include('README.md')
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
15
|
+
end
|
16
|
+
|
17
|
+
APP_RAKEFILE = File.expand_path('test/dummy/Rakefile', __dir__)
|
18
|
+
load 'rails/tasks/engine.rake'
|
19
|
+
|
20
|
+
load 'rails/tasks/statistics.rake'
|
21
|
+
|
22
|
+
require 'bundler/gem_tasks'
|
23
|
+
|
24
|
+
require 'rake/testtask'
|
25
|
+
|
26
|
+
Rake::TestTask.new(:test) do |t|
|
27
|
+
t.libs << 'test'
|
28
|
+
t.pattern = 'test/**/*_test.rb'
|
29
|
+
t.verbose = false
|
30
|
+
end
|
31
|
+
|
32
|
+
task default: :test
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Console1984
|
2
|
+
class IncinerationJob < ActiveJob::Base
|
3
|
+
queue_as { Console1984.incineration_queue }
|
4
|
+
|
5
|
+
discard_on ActiveRecord::RecordNotFound
|
6
|
+
|
7
|
+
def self.schedule(session)
|
8
|
+
set(wait: Console1984.incinerate_after).perform_later(session)
|
9
|
+
end
|
10
|
+
|
11
|
+
def perform(session)
|
12
|
+
session.incinerate
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
ActiveSupport.run_load_hooks(:console_1984_incineration_job, Console1984::IncinerationJob)
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Console1984
|
2
|
+
class Command < Base
|
3
|
+
belongs_to :session
|
4
|
+
belongs_to :sensitive_access, optional: true
|
5
|
+
|
6
|
+
encrypts :statements
|
7
|
+
|
8
|
+
scope :sorted_chronologically, -> { order(created_at: :asc, id: :asc) }
|
9
|
+
|
10
|
+
def sensitive?
|
11
|
+
sensitive_access.present?
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Console1984
|
2
|
+
class Session < Base
|
3
|
+
include Incineratable
|
4
|
+
|
5
|
+
belongs_to :user
|
6
|
+
has_many :commands, dependent: :destroy
|
7
|
+
has_many :sensitive_accesses, dependent: :destroy
|
8
|
+
|
9
|
+
def sensitive?
|
10
|
+
sensitive_accesses.any?
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
ActiveSupport.run_load_hooks(:console_1984_session, Console1984::Session)
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Console1984::Session::Incineratable
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
included do
|
5
|
+
after_create_commit :incinerate_later, if: -> { Console1984.incinerate }
|
6
|
+
end
|
7
|
+
|
8
|
+
def incinerate_later
|
9
|
+
Console1984::IncinerationJob.schedule self
|
10
|
+
end
|
11
|
+
|
12
|
+
def incinerate
|
13
|
+
if incineratable?
|
14
|
+
destroy
|
15
|
+
else
|
16
|
+
raise Console1984::Errors::ForbiddenIncineration,
|
17
|
+
"Session #{id} was created at #{created_at.utc}. It shouldn't be deleted"\
|
18
|
+
" until #{earliest_possible_incineration_date.utc}, and now it's #{Time.now.utc}"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
def incineratable?
|
24
|
+
Time.now >= earliest_possible_incineration_date
|
25
|
+
end
|
26
|
+
|
27
|
+
def earliest_possible_incineration_date
|
28
|
+
created_at + Console1984.incinerate_after
|
29
|
+
end
|
30
|
+
end
|
data/config/routes.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
class CreateConsole1984Tables < ActiveRecord::Migration[7.0]
|
2
|
+
def change
|
3
|
+
create_table :console1984_sessions do |t|
|
4
|
+
t.text :reason
|
5
|
+
t.references :user, null: false
|
6
|
+
t.timestamps
|
7
|
+
|
8
|
+
t.index :created_at
|
9
|
+
t.index [ :user_id, :created_at ]
|
10
|
+
end
|
11
|
+
|
12
|
+
create_table :console1984_users do |t|
|
13
|
+
t.string :username, null: false
|
14
|
+
t.timestamps
|
15
|
+
|
16
|
+
t.index [:username]
|
17
|
+
end
|
18
|
+
|
19
|
+
create_table :console1984_commands do |t|
|
20
|
+
t.text :statements
|
21
|
+
t.references :sensitive_access
|
22
|
+
t.references :session, null: false
|
23
|
+
t.timestamps
|
24
|
+
|
25
|
+
t.index [ :session_id, :created_at, :sensitive_access_id ], name: "on_session_and_sensitive_chronologically"
|
26
|
+
end
|
27
|
+
|
28
|
+
create_table :console1984_sensitive_accesses do |t|
|
29
|
+
t.text :justification
|
30
|
+
t.references :session, null: false
|
31
|
+
|
32
|
+
t.timestamps
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/lib/console1984.rb
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'console1984/engine'
|
2
|
+
|
3
|
+
require "zeitwerk"
|
4
|
+
loader = Zeitwerk::Loader.for_gem
|
5
|
+
loader.setup
|
6
|
+
|
7
|
+
module Console1984
|
8
|
+
include Messages
|
9
|
+
|
10
|
+
mattr_accessor :supervisor
|
11
|
+
mattr_accessor :session_logger
|
12
|
+
mattr_accessor :username_resolver
|
13
|
+
|
14
|
+
mattr_accessor :protected_environments
|
15
|
+
mattr_reader :protected_urls, default: []
|
16
|
+
|
17
|
+
mattr_reader :production_data_warning, default: DEFAULT_PRODUCTION_DATA_WARNING
|
18
|
+
mattr_reader :enter_unprotected_encryption_mode_warning, default: DEFAULT_ENTER_UNPROTECTED_ENCRYPTION_MODE_WARNING
|
19
|
+
mattr_reader :enter_protected_mode_warning, default: DEFAULT_ENTER_PROTECTED_MODE_WARNING
|
20
|
+
|
21
|
+
mattr_accessor :incinerate, default: true
|
22
|
+
mattr_accessor :incinerate_after, default: 30.days
|
23
|
+
mattr_accessor :incineration_queue, default: "console1984_incineration"
|
24
|
+
|
25
|
+
mattr_accessor :debug, default: false
|
26
|
+
|
27
|
+
thread_mattr_accessor :currently_protected_urls, default: []
|
28
|
+
|
29
|
+
class << self
|
30
|
+
def install_support(config)
|
31
|
+
self.protected_environments ||= config.protected_environments
|
32
|
+
self.protected_urls.push(*config.protected_urls)
|
33
|
+
self.session_logger = config.session_logger || Console1984::SessionsLogger::Database.new
|
34
|
+
self.username_resolver = config.username_resolver || Console1984::Username::EnvResolver.new("CONSOLE_USER")
|
35
|
+
|
36
|
+
self.supervisor = Supervisor.new
|
37
|
+
self.protected_urls.freeze
|
38
|
+
|
39
|
+
extend_protected_systems
|
40
|
+
end
|
41
|
+
|
42
|
+
def running_protected_environment?
|
43
|
+
protected_environments.collect(&:to_sym).include?(Rails.env.to_sym)
|
44
|
+
end
|
45
|
+
|
46
|
+
def protecting(&block)
|
47
|
+
protecting_connections do
|
48
|
+
ActiveRecord::Encryption.protecting_encrypted_data(&block)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
def extend_protected_systems
|
54
|
+
extend_active_record
|
55
|
+
extend_socket_classes
|
56
|
+
end
|
57
|
+
|
58
|
+
def extend_active_record
|
59
|
+
%w[ActiveRecord::ConnectionAdapters::Mysql2Adapter ActiveRecord::ConnectionAdapters::PostgreSQLAdapter ActiveRecord::ConnectionAdapters::SQLite3Adapter].each do |class_string|
|
60
|
+
if Object.const_defined?(class_string)
|
61
|
+
klass = class_string.constantize
|
62
|
+
klass.prepend(Console1984::ProtectedAuditableTables)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def extend_socket_classes
|
68
|
+
socket_classes = [TCPSocket, OpenSSL::SSL::SSLSocket]
|
69
|
+
if defined?(Redis::Connection)
|
70
|
+
socket_classes.push(*[Redis::Connection::TCPSocket, Redis::Connection::SSLSocket])
|
71
|
+
end
|
72
|
+
|
73
|
+
socket_classes.compact.each do |socket_klass|
|
74
|
+
socket_klass.prepend Console1984::ProtectedTcpSocket
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def protecting_connections
|
79
|
+
old_currently_protected_urls = self.currently_protected_urls
|
80
|
+
self.currently_protected_urls = protected_urls
|
81
|
+
yield
|
82
|
+
ensure
|
83
|
+
self.currently_protected_urls = old_currently_protected_urls
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'irb'
|
2
|
+
|
3
|
+
module Console1984
|
4
|
+
class Engine < ::Rails::Engine
|
5
|
+
isolate_namespace Console1984
|
6
|
+
|
7
|
+
config.console1984 = ActiveSupport::OrderedOptions.new
|
8
|
+
config.console1984.protected_environments ||= %i[ production ]
|
9
|
+
config.console1984.protected_urls ||= []
|
10
|
+
|
11
|
+
initializer "console1984.config" do
|
12
|
+
config.console1984.each do |key, value|
|
13
|
+
Console1984.send("#{key}=", value) unless %i[ protected_urls protected_environments ].include?(key.to_sym)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
console do
|
18
|
+
Console1984.install_support(config.console1984)
|
19
|
+
Console1984.supervisor.start if Console1984.running_protected_environment?
|
20
|
+
|
21
|
+
class OpenSSL::SSL::SSLSocket
|
22
|
+
# Make it serve remote address as TCPSocket so that our extension works for it
|
23
|
+
def remote_address
|
24
|
+
Addrinfo.getaddrinfo(hostname, 443).first
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Console1984
|
2
|
+
module Errors
|
3
|
+
class ProtectedConnection < StandardError
|
4
|
+
def initialize(details)
|
5
|
+
super "A connection attempt was prevented because it represents a sensitive access."\
|
6
|
+
"Please run decrypt! and try again. You will be asked to justify this access: #{details}"
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
class ForbiddenCommand < StandardError; end
|
11
|
+
class ForbiddenIncineration < StandardError; end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'colorized_string'
|
2
|
+
|
3
|
+
module Console1984::Messages
|
4
|
+
DEFAULT_PRODUCTION_DATA_WARNING = <<~TXT
|
5
|
+
|
6
|
+
You have access to production data here. That's a big deal. As part of our promise to keep customer data safe and private, we audit the commands you type here. Let's get started!
|
7
|
+
TXT
|
8
|
+
|
9
|
+
DEFAULT_ENTER_UNPROTECTED_ENCRYPTION_MODE_WARNING = <<~TXT
|
10
|
+
Ok! You have access to encrypted information now. We pay extra close attention to any commands entered while you have this access. You can go back to protected mode with 'encrypt!'
|
11
|
+
|
12
|
+
WARNING: Make sure you don't save objects that were loaded while in protected mode, as this can result in saving the encrypted texts.
|
13
|
+
TXT
|
14
|
+
|
15
|
+
DEFAULT_ENTER_PROTECTED_MODE_WARNING = <<~TXT
|
16
|
+
Great! You are back in protected mode. When we audit, we may reach out for a conversation about the commands you entered. What went well? Did you solve the problem without accessing personal data?
|
17
|
+
TXT
|
18
|
+
|
19
|
+
COMMANDS = {
|
20
|
+
"decrypt!": "enter unprotected mode with access to encrypted information",
|
21
|
+
"log '<reason>'": "provide further information about what you are going to do in the middle of a console session"
|
22
|
+
}
|
23
|
+
|
24
|
+
COMMANDS_HELP = <<~TXT
|
25
|
+
|
26
|
+
Commands:
|
27
|
+
|
28
|
+
#{COMMANDS.collect { |command, help_line| "* #{ColorizedString.new(command.to_s).light_blue}: #{help_line}" }.join("\n")}
|
29
|
+
|
30
|
+
TXT
|
31
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Console1984
|
2
|
+
module ProtectedAuditableTables
|
3
|
+
%i[ execute exec_query exec_insert exec_delete exec_update exec_insert_all ].each do |method|
|
4
|
+
define_method method do |*args|
|
5
|
+
sql = args.first
|
6
|
+
if Console1984.supervisor.executing_user_command? && sql =~ auditable_tables_regexp
|
7
|
+
raise Console1984::Errors::ForbiddenCommand, "#{sql}"
|
8
|
+
else
|
9
|
+
super(*args)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
AUDITABLE_MODELS = [ Console1984::User, Console1984::Session, Console1984::Command, Console1984::SensitiveAccess ]
|
16
|
+
|
17
|
+
def auditable_tables_regexp
|
18
|
+
@auditable_tables_regexp ||= Regexp.new("#{auditable_tables.join("|")}")
|
19
|
+
end
|
20
|
+
|
21
|
+
def auditable_tables
|
22
|
+
# TODO: Not using Console1984::Base.descendants during development to make this work without eager loading
|
23
|
+
@auditable_tables ||= AUDITABLE_MODELS.collect(&:table_name)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Console1984::ProtectedContext
|
2
|
+
# Protect the code to show inspected objects too. This method is invoked
|
3
|
+
# for showing returned objects in the console
|
4
|
+
def inspect_last_value
|
5
|
+
Console1984.supervisor.execute do
|
6
|
+
super
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def evaluate(line, line_no, exception: nil)
|
11
|
+
Console1984.supervisor.execute_supervised(Array(line)) do
|
12
|
+
super
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Console1984::ProtectedTcpSocket
|
2
|
+
def write(*args)
|
3
|
+
protecting do
|
4
|
+
super
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
def write_nonblock(*args)
|
9
|
+
protecting do
|
10
|
+
super
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
def protecting
|
16
|
+
if protected?
|
17
|
+
raise Console1984::Errors::ProtectedConnection, remote_address.inspect
|
18
|
+
else
|
19
|
+
yield
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def protected?
|
24
|
+
protected_addresses&.include?(ComparableAddress.new(remote_address))
|
25
|
+
end
|
26
|
+
|
27
|
+
def protected_addresses
|
28
|
+
@protected_addresses ||= Console1984.currently_protected_urls.collect do |url|
|
29
|
+
host, port = host_and_port_from(url)
|
30
|
+
Array(Addrinfo.getaddrinfo(host, port)).collect { |addrinfo| ComparableAddress.new(addrinfo) if addrinfo.ip_address }
|
31
|
+
end.flatten.compact.uniq
|
32
|
+
end
|
33
|
+
|
34
|
+
def host_and_port_from(url)
|
35
|
+
URI(url).then do |parsed_uri|
|
36
|
+
if parsed_uri.host
|
37
|
+
[parsed_uri.host, parsed_uri.port]
|
38
|
+
else
|
39
|
+
host_and_port_from_invalid_uri(url)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
rescue URI::InvalidURIError
|
43
|
+
host_and_port_from_invalid_uri(url)
|
44
|
+
end
|
45
|
+
|
46
|
+
def host_and_port_from_invalid_uri(url)
|
47
|
+
host, _, port = url.rpartition(':')
|
48
|
+
[host, port]
|
49
|
+
end
|
50
|
+
|
51
|
+
ComparableAddress = Struct.new(:ip, :port) do
|
52
|
+
def initialize(addrinfo)
|
53
|
+
super(addrinfo.ip_address, addrinfo.ip_port)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
class Console1984::SessionsLogger::Database
|
2
|
+
attr_reader :current_session, :current_sensitive_access
|
3
|
+
|
4
|
+
def start_session(username, reason)
|
5
|
+
silence_logging do
|
6
|
+
user = Console1984::User.create_or_find_by!(username: username)
|
7
|
+
@current_session = user.sessions.create! reason: reason
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def finish_session
|
12
|
+
@current_session = nil
|
13
|
+
@current_sensitive_access = nil
|
14
|
+
end
|
15
|
+
|
16
|
+
def start_sensitive_access(justification)
|
17
|
+
silence_logging do
|
18
|
+
@current_sensitive_access = current_session.sensitive_accesses.create! justification: justification
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def end_sensitive_access
|
23
|
+
@current_sensitive_access = nil
|
24
|
+
end
|
25
|
+
|
26
|
+
def before_executing(statements)
|
27
|
+
silence_logging do
|
28
|
+
@before_commands_count = @current_session.commands.count
|
29
|
+
record_statements statements
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def after_executing(statements)
|
34
|
+
end
|
35
|
+
|
36
|
+
def suspicious_commands_attempted(statements)
|
37
|
+
silence_logging do
|
38
|
+
sensitive_access = start_sensitive_access "Suspicious commands attempted"
|
39
|
+
Console1984::Command.last.update! sensitive_access: sensitive_access
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
def record_statements(statements)
|
45
|
+
@current_session.commands.create! statements: Array(statements).join("\n"), sensitive_access: current_sensitive_access
|
46
|
+
end
|
47
|
+
|
48
|
+
def silence_logging(&block)
|
49
|
+
if Console1984.debug
|
50
|
+
block.call
|
51
|
+
else
|
52
|
+
Console1984::IncinerationJob.logger.silence do
|
53
|
+
Console1984::Base.logger.silence(&block)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'colorized_string'
|
2
|
+
require 'rails/console/app'
|
3
|
+
|
4
|
+
class Console1984::Supervisor
|
5
|
+
include Accesses, InputOutput, Executor
|
6
|
+
|
7
|
+
attr_reader :session_id
|
8
|
+
delegate :session_logger, :username_resolver, to: Console1984
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
disable_access_to_encrypted_content(silent: true)
|
12
|
+
end
|
13
|
+
|
14
|
+
def start
|
15
|
+
show_production_data_warning
|
16
|
+
show_commands
|
17
|
+
|
18
|
+
extend_irb
|
19
|
+
|
20
|
+
session_logger.start_session current_username, ask_for_session_reason
|
21
|
+
end
|
22
|
+
|
23
|
+
def stop
|
24
|
+
session_logger.finish_session
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
def current_username
|
29
|
+
username_resolver.current
|
30
|
+
end
|
31
|
+
|
32
|
+
def show_production_data_warning
|
33
|
+
show_warning Console1984.production_data_warning
|
34
|
+
end
|
35
|
+
|
36
|
+
def extend_irb
|
37
|
+
IRB::Context.prepend(Console1984::ProtectedContext)
|
38
|
+
Rails::ConsoleMethods.include(Console1984::Commands)
|
39
|
+
end
|
40
|
+
|
41
|
+
def ask_for_session_reason
|
42
|
+
ask_for_value("#{current_username}, why are you using this console today?")
|
43
|
+
end
|
44
|
+
|
45
|
+
def show_commands
|
46
|
+
puts COMMANDS_HELP
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Console1984::Supervisor::Accesses
|
2
|
+
include Console1984::Messages
|
3
|
+
|
4
|
+
PROTECTED_ACCESS = Protected.new
|
5
|
+
UNPROTECTED_ACCESS = Unprotected.new
|
6
|
+
|
7
|
+
def enable_access_to_encrypted_content(silent: false)
|
8
|
+
run_system_command do
|
9
|
+
show_warning Console1984.enter_unprotected_encryption_mode_warning if !silent && protected_mode?
|
10
|
+
justification = ask_for_value "\nBefore you can access personal information, you need to ask for and get explicit consent from the user(s). #{current_username}, where can we find this consent (a URL would be great)?"
|
11
|
+
session_logger.start_sensitive_access justification
|
12
|
+
nil
|
13
|
+
end
|
14
|
+
ensure
|
15
|
+
@access = UNPROTECTED_ACCESS
|
16
|
+
nil
|
17
|
+
end
|
18
|
+
|
19
|
+
def disable_access_to_encrypted_content(silent: false)
|
20
|
+
run_system_command do
|
21
|
+
show_warning Console1984.enter_protected_mode_warning if !silent && unprotected_mode?
|
22
|
+
session_logger.end_sensitive_access
|
23
|
+
nil
|
24
|
+
end
|
25
|
+
ensure
|
26
|
+
@access = PROTECTED_ACCESS
|
27
|
+
nil
|
28
|
+
end
|
29
|
+
|
30
|
+
def with_encryption_mode(&block)
|
31
|
+
@access.execute(&block)
|
32
|
+
end
|
33
|
+
|
34
|
+
def unprotected_mode?
|
35
|
+
@access.is_a?(Unprotected)
|
36
|
+
end
|
37
|
+
|
38
|
+
def protected_mode?
|
39
|
+
!unprotected_mode?
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Console1984::Supervisor::Executor
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
def execute_supervised(commands, &block)
|
5
|
+
run_system_command { session_logger.before_executing commands }
|
6
|
+
execute(&block)
|
7
|
+
rescue Console1984::Errors::ForbiddenCommand
|
8
|
+
puts "Forbidden command attempted: #{commands.join("\n")}"
|
9
|
+
run_system_command { session_logger.suspicious_commands_attempted commands }
|
10
|
+
nil
|
11
|
+
ensure
|
12
|
+
run_system_command { session_logger.after_executing commands }
|
13
|
+
end
|
14
|
+
|
15
|
+
def execute(&block)
|
16
|
+
run_user_command do
|
17
|
+
with_encryption_mode(&block)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def executing_user_command?
|
22
|
+
@executing_user_command
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
def run_user_command(&block)
|
27
|
+
run_command true, &block
|
28
|
+
end
|
29
|
+
|
30
|
+
def run_system_command(&block)
|
31
|
+
run_command false, &block
|
32
|
+
end
|
33
|
+
|
34
|
+
def run_command(run_by_user, &block)
|
35
|
+
original_value = @executing_user_command
|
36
|
+
@executing_user_command = run_by_user
|
37
|
+
block.call
|
38
|
+
ensure
|
39
|
+
@executing_user_command = original_value
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Console1984::Supervisor::InputOutput
|
2
|
+
def show_warning(message)
|
3
|
+
puts ColorizedString.new("\n#{message}\n").yellow
|
4
|
+
end
|
5
|
+
|
6
|
+
def ask_for_value(message)
|
7
|
+
puts ColorizedString.new("#{message}").green
|
8
|
+
reason = $stdin.gets.strip until reason.present?
|
9
|
+
reason
|
10
|
+
end
|
11
|
+
end
|
metadata
ADDED
@@ -0,0 +1,186 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: console1984
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jorge Manrubia
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-08-18 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: colorize
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: benchmark-ips
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: mocha
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rubocop
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 1.18.4
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 1.18.4
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rubocop-performance
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rubocop-packaging
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rubocop-rails
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: sqlite3
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
description:
|
126
|
+
email:
|
127
|
+
- jorge@basecamp.com
|
128
|
+
executables: []
|
129
|
+
extensions: []
|
130
|
+
extra_rdoc_files: []
|
131
|
+
files:
|
132
|
+
- MIT-LICENSE
|
133
|
+
- README.md
|
134
|
+
- Rakefile
|
135
|
+
- app/jobs/console1984/incineration_job.rb
|
136
|
+
- app/models/console1984/base.rb
|
137
|
+
- app/models/console1984/command.rb
|
138
|
+
- app/models/console1984/sensitive_access.rb
|
139
|
+
- app/models/console1984/session.rb
|
140
|
+
- app/models/console1984/session/incineratable.rb
|
141
|
+
- app/models/console1984/user.rb
|
142
|
+
- config/routes.rb
|
143
|
+
- db/migrate/20210517203931_create_console1984_tables.rb
|
144
|
+
- lib/console1984.rb
|
145
|
+
- lib/console1984/commands.rb
|
146
|
+
- lib/console1984/engine.rb
|
147
|
+
- lib/console1984/env_variable_username.rb
|
148
|
+
- lib/console1984/errors.rb
|
149
|
+
- lib/console1984/messages.rb
|
150
|
+
- lib/console1984/protected_auditable_tables.rb
|
151
|
+
- lib/console1984/protected_context.rb
|
152
|
+
- lib/console1984/protected_tcp_socket.rb
|
153
|
+
- lib/console1984/sessions_logger/database.rb
|
154
|
+
- lib/console1984/supervisor.rb
|
155
|
+
- lib/console1984/supervisor/accesses.rb
|
156
|
+
- lib/console1984/supervisor/accesses/protected.rb
|
157
|
+
- lib/console1984/supervisor/accesses/unprotected.rb
|
158
|
+
- lib/console1984/supervisor/executor.rb
|
159
|
+
- lib/console1984/supervisor/input_output.rb
|
160
|
+
- lib/console1984/username/env_resolver.rb
|
161
|
+
- lib/console1984/version.rb
|
162
|
+
homepage: http://github.com/basecamp/console1984
|
163
|
+
licenses:
|
164
|
+
- MIT
|
165
|
+
metadata:
|
166
|
+
allowed_push_host: https://rubygems.org
|
167
|
+
post_install_message:
|
168
|
+
rdoc_options: []
|
169
|
+
require_paths:
|
170
|
+
- lib
|
171
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
172
|
+
requirements:
|
173
|
+
- - ">="
|
174
|
+
- !ruby/object:Gem::Version
|
175
|
+
version: '0'
|
176
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
177
|
+
requirements:
|
178
|
+
- - ">="
|
179
|
+
- !ruby/object:Gem::Version
|
180
|
+
version: '0'
|
181
|
+
requirements: []
|
182
|
+
rubygems_version: 3.1.4
|
183
|
+
signing_key:
|
184
|
+
specification_version: 4
|
185
|
+
summary: Your Rails console, 1984 style
|
186
|
+
test_files: []
|