console1984 0.1.4 → 0.1.8
Sign up to get free protection for your applications and to get access to all the features.
- 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
|