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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +32 -10
  3. data/config/protections.yml +30 -0
  4. data/lib/console1984/command_executor.rb +90 -0
  5. data/lib/console1984/command_validator/forbidden_constant_reference_validation.rb +31 -0
  6. data/lib/console1984/command_validator/forbidden_reopening_validation.rb +29 -0
  7. data/lib/console1984/command_validator/parsed_command.rb +90 -0
  8. data/lib/console1984/command_validator/suspicious_terms_validation.rb +22 -0
  9. data/lib/console1984/command_validator.rb +71 -0
  10. data/lib/console1984/config.rb +21 -9
  11. data/lib/console1984/engine.rb +6 -8
  12. data/lib/console1984/errors.rb +10 -1
  13. data/lib/console1984/ext/active_record/protected_auditable_tables.rb +28 -0
  14. data/lib/console1984/ext/core/module.rb +15 -0
  15. data/lib/console1984/ext/core/object.rb +43 -0
  16. data/lib/console1984/ext/irb/commands.rb +16 -0
  17. data/lib/console1984/ext/irb/context.rb +20 -0
  18. data/lib/console1984/{protected_tcp_socket.rb → ext/socket/tcp_socket.rb} +10 -4
  19. data/lib/console1984/freezeable.rb +70 -0
  20. data/lib/console1984/{supervisor/input_output.rb → input_output.rb} +9 -3
  21. data/lib/console1984/messages.rb +0 -10
  22. data/lib/console1984/protections_config.rb +17 -0
  23. data/lib/console1984/refrigerator.rb +32 -0
  24. data/lib/console1984/sessions_logger/database.rb +3 -1
  25. data/lib/console1984/shield/method_invocation_shell.rb +52 -0
  26. data/lib/console1984/shield/modes/protected.rb +27 -0
  27. data/lib/console1984/shield/modes/unprotected.rb +8 -0
  28. data/lib/console1984/shield/modes.rb +60 -0
  29. data/lib/console1984/shield.rb +85 -0
  30. data/lib/console1984/supervisor.rb +27 -22
  31. data/lib/console1984/username/env_resolver.rb +2 -0
  32. data/lib/console1984/version.rb +1 -1
  33. data/lib/console1984.rb +43 -21
  34. metadata +66 -14
  35. data/config/routes.rb +0 -9
  36. data/lib/console1984/commands.rb +0 -16
  37. data/lib/console1984/frozen_methods.rb +0 -17
  38. data/lib/console1984/protected_auditable_tables.rb +0 -29
  39. data/lib/console1984/protected_context.rb +0 -18
  40. data/lib/console1984/supervisor/accesses/protected.rb +0 -10
  41. data/lib/console1984/supervisor/accesses/unprotected.rb +0 -5
  42. data/lib/console1984/supervisor/accesses.rb +0 -41
  43. data/lib/console1984/supervisor/executor.rb +0 -41
  44. data/lib/console1984/supervisor/protector.rb +0 -37
@@ -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
- debug freeze_config
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 if freeze_config
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.freeze_config = true
59
+ self.test_mode = false
48
60
  end
49
61
  end
@@ -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) unless %i[ protected_urls protected_environments ].include?(key.to_sym)
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
- Console1984.supervisor.start if Console1984.running_protected_environment?
21
-
22
- class OpenSSL::SSL::SSLSocket
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
@@ -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::ProtectedTcpSocket
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 ||= Console1984.currently_protected_urls.collect do |url|
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::FrozenMethods
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::Supervisor::InputOutput
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 COMMANDS_HELP
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)
@@ -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