console1984 0.1.4 → 0.1.8

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