console1984 0.1.4 → 0.1.8
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/README.md +32 -10
- data/config/protections.yml +30 -0
- data/lib/console1984/command_executor.rb +90 -0
- data/lib/console1984/command_validator/forbidden_constant_reference_validation.rb +31 -0
- data/lib/console1984/command_validator/forbidden_reopening_validation.rb +29 -0
- data/lib/console1984/command_validator/parsed_command.rb +90 -0
- data/lib/console1984/command_validator/suspicious_terms_validation.rb +22 -0
- data/lib/console1984/command_validator.rb +71 -0
- data/lib/console1984/config.rb +21 -9
- data/lib/console1984/engine.rb +6 -8
- data/lib/console1984/errors.rb +10 -1
- data/lib/console1984/ext/active_record/protected_auditable_tables.rb +28 -0
- data/lib/console1984/ext/core/module.rb +15 -0
- data/lib/console1984/ext/core/object.rb +43 -0
- data/lib/console1984/ext/irb/commands.rb +16 -0
- data/lib/console1984/ext/irb/context.rb +20 -0
- data/lib/console1984/{protected_tcp_socket.rb → ext/socket/tcp_socket.rb} +10 -4
- data/lib/console1984/freezeable.rb +70 -0
- data/lib/console1984/{supervisor/input_output.rb → input_output.rb} +9 -3
- data/lib/console1984/messages.rb +0 -10
- data/lib/console1984/protections_config.rb +17 -0
- data/lib/console1984/refrigerator.rb +32 -0
- data/lib/console1984/sessions_logger/database.rb +3 -1
- data/lib/console1984/shield/method_invocation_shell.rb +52 -0
- data/lib/console1984/shield/modes/protected.rb +27 -0
- data/lib/console1984/shield/modes/unprotected.rb +8 -0
- data/lib/console1984/shield/modes.rb +60 -0
- data/lib/console1984/shield.rb +85 -0
- data/lib/console1984/supervisor.rb +27 -22
- data/lib/console1984/username/env_resolver.rb +2 -0
- data/lib/console1984/version.rb +1 -1
- data/lib/console1984.rb +43 -21
- metadata +66 -14
- data/config/routes.rb +0 -9
- data/lib/console1984/commands.rb +0 -16
- data/lib/console1984/frozen_methods.rb +0 -17
- data/lib/console1984/protected_auditable_tables.rb +0 -29
- data/lib/console1984/protected_context.rb +0 -18
- data/lib/console1984/supervisor/accesses/protected.rb +0 -10
- data/lib/console1984/supervisor/accesses/unprotected.rb +0 -5
- data/lib/console1984/supervisor/accesses.rb +0 -41
- data/lib/console1984/supervisor/executor.rb +0 -41
- data/lib/console1984/supervisor/protector.rb +0 -37
data/lib/console1984/config.rb
CHANGED
@@ -1,13 +1,18 @@
|
|
1
1
|
# Container for config options.
|
2
|
+
#
|
3
|
+
# These config options are accessible via first-level reader methods at Console1984.
|
2
4
|
class Console1984::Config
|
3
|
-
include Console1984::Messages
|
5
|
+
include Console1984::Freezeable, Console1984::Messages
|
6
|
+
|
7
|
+
PROTECTIONS_CONFIG_FILE_PATH = Console1984::Engine.root.join("config/protections.yml")
|
4
8
|
|
5
9
|
PROPERTIES = %i[
|
6
|
-
session_logger username_resolver
|
10
|
+
session_logger username_resolver shield command_executor
|
7
11
|
protected_environments protected_urls
|
8
12
|
production_data_warning enter_unprotected_encryption_mode_warning enter_protected_mode_warning
|
9
13
|
incinerate incinerate_after incineration_queue
|
10
|
-
|
14
|
+
protections_config
|
15
|
+
debug test_mode
|
11
16
|
]
|
12
17
|
|
13
18
|
attr_accessor(*PROPERTIES)
|
@@ -22,18 +27,25 @@ class Console1984::Config
|
|
22
27
|
end
|
23
28
|
end
|
24
29
|
|
30
|
+
# Initialize lazily so that it only gets instantiated during console sessions
|
31
|
+
def protections_config
|
32
|
+
@protections_config ||= Console1984::ProtectionsConfig.new(YAML.safe_load(File.read(PROTECTIONS_CONFIG_FILE_PATH)).symbolize_keys)
|
33
|
+
end
|
34
|
+
|
25
35
|
def freeze
|
26
|
-
super
|
27
|
-
protected_urls.freeze
|
36
|
+
super
|
37
|
+
[ protected_urls, protections_config ].each(&:freeze)
|
28
38
|
end
|
29
39
|
|
30
40
|
private
|
31
41
|
def set_defaults
|
32
|
-
self.protected_environments = []
|
33
|
-
self.protected_urls = []
|
34
|
-
|
35
42
|
self.session_logger = Console1984::SessionsLogger::Database.new
|
36
43
|
self.username_resolver = Console1984::Username::EnvResolver.new("CONSOLE_USER")
|
44
|
+
self.shield = Console1984::Shield.new
|
45
|
+
self.command_executor = Console1984::CommandExecutor.new
|
46
|
+
|
47
|
+
self.protected_environments = []
|
48
|
+
self.protected_urls = []
|
37
49
|
|
38
50
|
self.production_data_warning = DEFAULT_PRODUCTION_DATA_WARNING
|
39
51
|
self.enter_unprotected_encryption_mode_warning = DEFAULT_ENTER_UNPROTECTED_ENCRYPTION_MODE_WARNING
|
@@ -44,6 +56,6 @@ class Console1984::Config
|
|
44
56
|
self.incineration_queue = "console1984_incineration"
|
45
57
|
|
46
58
|
self.debug = false
|
47
|
-
self.
|
59
|
+
self.test_mode = false
|
48
60
|
end
|
49
61
|
end
|
data/lib/console1984/engine.rb
CHANGED
@@ -10,20 +10,18 @@ module Console1984
|
|
10
10
|
|
11
11
|
initializer "console1984.config" do
|
12
12
|
config.console1984.each do |key, value|
|
13
|
-
Console1984.config.send("#{key}=", value)
|
13
|
+
Console1984.config.send("#{key}=", value)
|
14
14
|
end
|
15
15
|
end
|
16
16
|
|
17
|
+
# Console 1984 setup happens when a console is started. Just including the
|
18
|
+
# gem won't install any protection mechanisms.
|
17
19
|
console do
|
18
20
|
Console1984.config.set_from(config.console1984)
|
19
21
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
# Make it serve remote address as TCPSocket so that our extension works for it
|
24
|
-
def remote_address
|
25
|
-
Addrinfo.getaddrinfo(hostname, 443).first
|
26
|
-
end
|
22
|
+
if Console1984.running_protected_environment?
|
23
|
+
Console1984.supervisor.install
|
24
|
+
Console1984.supervisor.start
|
27
25
|
end
|
28
26
|
end
|
29
27
|
end
|
data/lib/console1984/errors.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
module Console1984
|
2
2
|
module Errors
|
3
|
+
# Attempt to access a protected url while in protected mode.
|
3
4
|
class ProtectedConnection < StandardError
|
4
5
|
def initialize(details)
|
5
6
|
super "A connection attempt was prevented because it represents a sensitive access."\
|
@@ -7,8 +8,16 @@ module Console1984
|
|
7
8
|
end
|
8
9
|
end
|
9
10
|
|
11
|
+
# Attempt to execute a command that is not allowed. The system won't
|
12
|
+
# execute such commands and will flag them as sensitive.
|
10
13
|
class ForbiddenCommand < StandardError; end
|
14
|
+
|
15
|
+
# A suspicious command was executed. The command will be flagged but the system
|
16
|
+
# will let it run.
|
17
|
+
class SuspiciousCommand < StandardError; end
|
18
|
+
|
19
|
+
# Attempt to incinerate a session ahead of time as determined by
|
20
|
+
# +config.console1984.incinerate_after+.
|
11
21
|
class ForbiddenIncineration < StandardError; end
|
12
|
-
class ForbiddenClassManipulation < StandardError; end
|
13
22
|
end
|
14
23
|
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# Prevents accessing trail model tables when executing console commands.
|
2
|
+
module Console1984::Ext::ActiveRecord::ProtectedAuditableTables
|
3
|
+
include Console1984::Freezeable
|
4
|
+
|
5
|
+
%i[ execute exec_query exec_insert exec_delete exec_update exec_insert_all ].each do |method|
|
6
|
+
define_method method do |*args, **kwargs|
|
7
|
+
sql = args.first
|
8
|
+
if Console1984.command_executor.executing_user_command? && sql =~ auditable_tables_regexp
|
9
|
+
raise Console1984::Errors::ForbiddenCommand, "#{sql}"
|
10
|
+
else
|
11
|
+
super(*args, **kwargs)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
def auditable_tables_regexp
|
18
|
+
@auditable_tables_regexp ||= Regexp.new("#{auditable_tables.join("|")}")
|
19
|
+
end
|
20
|
+
|
21
|
+
def auditable_tables
|
22
|
+
@auditable_tables ||= Console1984.command_executor.run_as_system { auditable_models.collect(&:table_name) }
|
23
|
+
end
|
24
|
+
|
25
|
+
def auditable_models
|
26
|
+
@auditable_models ||= Console1984::Base.descendants
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# Extends +Module+ to prevent invoking class_eval in user commands.
|
2
|
+
#
|
3
|
+
# We don't use the built-in configurable system from protections.yml because we use
|
4
|
+
# class_eval ourselves to implement it!
|
5
|
+
module Console1984::Ext::Core::Module
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
def instance_eval(*)
|
9
|
+
if Console1984.command_executor.executing_user_command?
|
10
|
+
raise Console1984::Errors::ForbiddenCommand
|
11
|
+
else
|
12
|
+
super
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# Prevents loading forbidden classes dynamically.
|
2
|
+
#
|
3
|
+
# There are classes that we don't want to allow loading dynamically
|
4
|
+
# during a console session. For example, we don't want users to reference
|
5
|
+
# the constant +Console1984+. We will prevent a direct constant reference
|
6
|
+
# but users could still do:
|
7
|
+
#
|
8
|
+
# MyConstant = ("Con" + "sole1984").constantize
|
9
|
+
#
|
10
|
+
# We prevent this by extending +Object#const_get+.
|
11
|
+
module Console1984::Ext::Core::Object
|
12
|
+
extend ActiveSupport::Concern
|
13
|
+
|
14
|
+
include Console1984::Freezeable
|
15
|
+
self.prevent_instance_data_manipulation_after_freezing = false
|
16
|
+
|
17
|
+
class_methods do
|
18
|
+
def const_get(*arguments)
|
19
|
+
if Console1984.command_executor.executing_user_command?
|
20
|
+
begin
|
21
|
+
# To validate if it's an invalid constant, we try to declare a class with it.
|
22
|
+
# We essentially leverage Console1984::CommandValidator::ForbiddenReopeningValidation here:
|
23
|
+
# We don't let referencing constants referring modules or classes we don't allow to extend.
|
24
|
+
#
|
25
|
+
# See the list +forbidden_reopening+ in +config/command_protections.yml+.
|
26
|
+
Console1984.command_executor.validate_command("class #{arguments.first}; end")
|
27
|
+
super
|
28
|
+
rescue Console1984::Errors::ForbiddenCommand
|
29
|
+
raise
|
30
|
+
rescue StandardError
|
31
|
+
super
|
32
|
+
end
|
33
|
+
else
|
34
|
+
super
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
def banned_dynamic_constant_declaration?(arguments)
|
41
|
+
Console1984.command_executor.validate_command("class #{arguments.first}; end")
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# Add Console 1984 commands to IRB sessions.
|
2
|
+
module Console1984::Ext::Irb::Commands
|
3
|
+
include Console1984::Freezeable
|
4
|
+
|
5
|
+
delegate :shield, to: Console1984
|
6
|
+
|
7
|
+
# Enter {unprotected mode}[rdoc-ref:Console1984::Shield::Modes] mode.
|
8
|
+
def decrypt!
|
9
|
+
shield.enable_unprotected_mode
|
10
|
+
end
|
11
|
+
|
12
|
+
# Enter {protected mode}[rdoc-ref:Console1984::Shield::Modes] mode.
|
13
|
+
def encrypt!
|
14
|
+
shield.enable_protected_mode
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# Extends IRB execution contexts to hijack execution attempts and
|
2
|
+
# pass them through Console1984.
|
3
|
+
module Console1984::Ext::Irb::Context
|
4
|
+
include Console1984::Freezeable
|
5
|
+
|
6
|
+
# This method is invoked for showing returned objects in the console
|
7
|
+
# Overridden to make sure their evaluation is supervised.
|
8
|
+
def inspect_last_value
|
9
|
+
Console1984.command_executor.execute_in_protected_mode do
|
10
|
+
super
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
#
|
15
|
+
def evaluate(line, line_no, exception: nil)
|
16
|
+
Console1984.command_executor.execute(Array(line)) do
|
17
|
+
super
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -1,5 +1,7 @@
|
|
1
|
-
# Wraps socket methods to execute supervised.
|
2
|
-
module Console1984::
|
1
|
+
# Wraps socket methods to execute supervised when {protected mode}[rdoc-ref:Console1984::Shield::Modes].
|
2
|
+
module Console1984::Ext::Socket::TcpSocket
|
3
|
+
include Console1984::Freezeable
|
4
|
+
|
3
5
|
def write(*args)
|
4
6
|
protecting do
|
5
7
|
super
|
@@ -26,12 +28,16 @@ module Console1984::ProtectedTcpSocket
|
|
26
28
|
end
|
27
29
|
|
28
30
|
def protected_addresses
|
29
|
-
@protected_addresses ||=
|
31
|
+
@protected_addresses ||= protected_urls.collect do |url|
|
30
32
|
host, port = host_and_port_from(url)
|
31
33
|
Array(Addrinfo.getaddrinfo(host, port)).collect { |addrinfo| ComparableAddress.new(addrinfo) if addrinfo.ip_address }
|
32
34
|
end.flatten.compact.uniq
|
33
35
|
end
|
34
36
|
|
37
|
+
def protected_urls
|
38
|
+
Console1984::Shield::Modes::PROTECTED_MODE.currently_protected_urls || []
|
39
|
+
end
|
40
|
+
|
35
41
|
def host_and_port_from(url)
|
36
42
|
URI(url).then do |parsed_uri|
|
37
43
|
if parsed_uri.host
|
@@ -55,5 +61,5 @@ module Console1984::ProtectedTcpSocket
|
|
55
61
|
end
|
56
62
|
end
|
57
63
|
|
58
|
-
include Console1984::
|
64
|
+
include Console1984::Freezeable
|
59
65
|
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# Prevents adding new methods to classes, changing class-state or
|
2
|
+
# accessing/overridden instance variables via reflection. This is meant to
|
3
|
+
# prevent manipulating certain Console1984 classes during a console session.
|
4
|
+
#
|
5
|
+
# Notice this won't prevent every state-modification command. You should
|
6
|
+
# handle special cases by overriding +#freeze+ (if necessary) and invoking
|
7
|
+
# freezing on the instance when it makes sense.
|
8
|
+
#
|
9
|
+
# For example: check Console1984::Config#freeze and Console1984::Shield#freeze_all.
|
10
|
+
#
|
11
|
+
# The "freezing" doesn't materialize when the mixin is included. When mixed in, it
|
12
|
+
# will store the host class or module in a list. Then a call to Console1984::Freezeable.freeze_all
|
13
|
+
# will look through all the modules/classes freezing them. This way, we can control
|
14
|
+
# the moment where we stop classes from being modifiable at setup time.
|
15
|
+
module Console1984::Freezeable
|
16
|
+
mattr_reader :to_freeze, default: Set.new
|
17
|
+
|
18
|
+
# Not using ActiveSupport::Concern because a bunch of classes skip its +.invoked+ hook which
|
19
|
+
# is terrible for our purposes. This happened because it was being included in parent classes
|
20
|
+
# (such as Object), so it was skipping the include block.
|
21
|
+
def self.included(base)
|
22
|
+
Console1984::Freezeable.to_freeze << base
|
23
|
+
base.extend ClassMethods
|
24
|
+
|
25
|
+
# Flag to control manipulating instance data via instance_variable_get and instance_variable_set.
|
26
|
+
# true by default.
|
27
|
+
base.thread_mattr_accessor :prevent_instance_data_manipulation_after_freezing, default: true
|
28
|
+
end
|
29
|
+
|
30
|
+
module ClassMethods
|
31
|
+
SENSITIVE_INSTANCE_METHODS = %i[ instance_variable_get instance_variable_set ]
|
32
|
+
|
33
|
+
def prevent_instance_data_manipulation
|
34
|
+
SENSITIVE_INSTANCE_METHODS.each do |method|
|
35
|
+
prevent_sensitive_method method
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
def prevent_sensitive_method(method_name)
|
41
|
+
define_method method_name do |*arguments|
|
42
|
+
raise Console1984::Errors::ForbiddenCommand, "You can't invoke #{method_name} on #{self}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class << self
|
48
|
+
def freeze_all
|
49
|
+
class_and_modules_to_freeze.each do |class_or_module|
|
50
|
+
freeze_class_or_module(class_or_module)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
def class_and_modules_to_freeze
|
56
|
+
with_descendants(to_freeze)
|
57
|
+
end
|
58
|
+
|
59
|
+
def freeze_class_or_module(class_or_module)
|
60
|
+
class_or_module.prevent_instance_data_manipulation if class_or_module.prevent_instance_data_manipulation_after_freezing
|
61
|
+
class_or_module.freeze
|
62
|
+
end
|
63
|
+
|
64
|
+
def with_descendants(classes_and_modules)
|
65
|
+
classes_and_modules + classes_and_modules.grep(Class).flat_map(&:descendants)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
freeze
|
70
|
+
end
|
@@ -1,5 +1,5 @@
|
|
1
|
-
module Console1984::
|
2
|
-
include Console1984::Messages
|
1
|
+
module Console1984::InputOutput
|
2
|
+
include Console1984::Freezeable, Console1984::Messages
|
3
3
|
|
4
4
|
private
|
5
5
|
def show_welcome_message
|
@@ -16,7 +16,13 @@ module Console1984::Supervisor::InputOutput
|
|
16
16
|
end
|
17
17
|
|
18
18
|
def show_commands
|
19
|
-
puts
|
19
|
+
puts <<~TXT
|
20
|
+
|
21
|
+
Commands:
|
22
|
+
|
23
|
+
#{COMMANDS.collect { |command, help_line| "* #{ColorizedString.new(command.to_s).light_blue}: #{help_line}" }.join("\n")}
|
24
|
+
|
25
|
+
TXT
|
20
26
|
end
|
21
27
|
|
22
28
|
def show_warning(message)
|
data/lib/console1984/messages.rb
CHANGED
@@ -1,5 +1,3 @@
|
|
1
|
-
require 'colorized_string'
|
2
|
-
|
3
1
|
module Console1984::Messages
|
4
2
|
DEFAULT_PRODUCTION_DATA_WARNING = <<~TXT
|
5
3
|
|
@@ -19,12 +17,4 @@ module Console1984::Messages
|
|
19
17
|
COMMANDS = {
|
20
18
|
"decrypt!": "enter unprotected mode with access to encrypted information"
|
21
19
|
}
|
22
|
-
|
23
|
-
COMMANDS_HELP = <<~TXT
|
24
|
-
|
25
|
-
Commands:
|
26
|
-
|
27
|
-
#{COMMANDS.collect { |command, help_line| "* #{ColorizedString.new(command.to_s).light_blue}: #{help_line}" }.join("\n")}
|
28
|
-
|
29
|
-
TXT
|
30
20
|
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class Console1984::ProtectionsConfig
|
2
|
+
include Console1984::Freezeable
|
3
|
+
|
4
|
+
delegate :static_validations, to: :instance
|
5
|
+
|
6
|
+
attr_reader :config
|
7
|
+
|
8
|
+
def initialize(config)
|
9
|
+
@config = config
|
10
|
+
end
|
11
|
+
|
12
|
+
%i[ static_validations forbidden_methods ].each do |method_name|
|
13
|
+
define_method method_name do
|
14
|
+
config[method_name].symbolize_keys
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# Freezes classes to prevent tampering them
|
2
|
+
class Console1984::Refrigerator
|
3
|
+
include Console1984::Freezeable
|
4
|
+
|
5
|
+
def freeze_all
|
6
|
+
eager_load_all_classes
|
7
|
+
freeze_internal_instances # internal modules and classes are frozen by including Console1984::Freezable
|
8
|
+
freeze_external_modules_and_classes
|
9
|
+
|
10
|
+
Console1984::Freezeable.freeze_all
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
EXTERNAL_MODULES_AND_CLASSES_TO_FREEZE = [Parser::CurrentRuby]
|
15
|
+
|
16
|
+
def freeze_internal_instances
|
17
|
+
Console1984.config.freeze unless Console1984.config.test_mode
|
18
|
+
end
|
19
|
+
|
20
|
+
def freeze_external_modules_and_classes
|
21
|
+
EXTERNAL_MODULES_AND_CLASSES_TO_FREEZE.each { |klass| klass.include(Console1984::Freezeable) }
|
22
|
+
end
|
23
|
+
|
24
|
+
def eager_load_all_classes
|
25
|
+
Rails.application.eager_load! unless Rails.application.config.eager_load
|
26
|
+
Console1984.class_loader.eager_load
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class Parser::Ruby27
|
31
|
+
include Console1984::Freezeable
|
32
|
+
end
|