console1984 0.1.6 → 0.1.7

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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +14 -1
  3. data/config/command_protections.yml +17 -0
  4. data/lib/console1984/command_executor.rb +94 -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 +64 -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 +9 -5
  11. data/lib/console1984/engine.rb +2 -0
  12. data/lib/console1984/errors.rb +10 -1
  13. data/lib/console1984/{protected_auditable_tables.rb → ext/active_record/protected_auditable_tables.rb} +2 -4
  14. data/lib/console1984/ext/core/object.rb +42 -0
  15. data/lib/console1984/ext/irb/commands.rb +16 -0
  16. data/lib/console1984/{protected_context.rb → ext/irb/context.rb} +5 -5
  17. data/lib/console1984/{protected_tcp_socket.rb → ext/socket/tcp_socket.rb} +3 -3
  18. data/lib/console1984/freezeable.rb +15 -5
  19. data/lib/console1984/{supervisor/input_output.rb → input_output.rb} +8 -2
  20. data/lib/console1984/messages.rb +0 -10
  21. data/lib/console1984/shield/modes/protected.rb +27 -0
  22. data/lib/console1984/shield/modes/unprotected.rb +8 -0
  23. data/lib/console1984/shield/modes.rb +60 -0
  24. data/lib/console1984/shield.rb +86 -0
  25. data/lib/console1984/supervisor.rb +24 -33
  26. data/lib/console1984/version.rb +1 -1
  27. data/lib/console1984.rb +36 -17
  28. metadata +61 -14
  29. data/config/routes.rb +0 -9
  30. data/lib/console1984/commands.rb +0 -16
  31. data/lib/console1984/protected_object.rb +0 -15
  32. data/lib/console1984/supervisor/accesses/protected.rb +0 -12
  33. data/lib/console1984/supervisor/accesses/unprotected.rb +0 -7
  34. data/lib/console1984/supervisor/accesses.rb +0 -41
  35. data/lib/console1984/supervisor/executor.rb +0 -65
  36. data/lib/console1984/supervisor/protector.rb +0 -55
@@ -0,0 +1,42 @@
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
+
16
+ class_methods do
17
+ def const_get(*arguments)
18
+ if Console1984.command_executor.executing_user_command?
19
+ begin
20
+ # To validate if it's an invalid constant, we try to declare a class with it.
21
+ # We essentially leverage Console1984::CommandValidator::ForbiddenReopeningValidation here:
22
+ # We don't let referencing constants referring modules or classes we don't allow to extend.
23
+ #
24
+ # See the list +forbidden_reopening+ in +config/command_protections.yml+.
25
+ Console1984.command_executor.validate_command("class #{arguments.first}; end")
26
+ super
27
+ rescue Console1984::Errors::ForbiddenCommand
28
+ raise
29
+ rescue StandardError
30
+ super
31
+ end
32
+ else
33
+ super
34
+ end
35
+ end
36
+ end
37
+
38
+ private
39
+ def banned_dynamic_constant_declaration?(arguments)
40
+ Console1984.command_executor.validate_command("class #{arguments.first}; end")
41
+ end
42
+ 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
@@ -1,20 +1,20 @@
1
- module Console1984::ProtectedContext
1
+ # Extends IRB execution contexts to hijack execution attempts and
2
+ # pass them through Console1984.
3
+ module Console1984::Ext::Irb::Context
2
4
  include Console1984::Freezeable
3
5
 
4
6
  # This method is invoked for showing returned objects in the console
5
7
  # Overridden to make sure their evaluation is supervised.
6
8
  def inspect_last_value
7
- Console1984.supervisor.execute do
9
+ Console1984.command_executor.execute_in_protected_mode do
8
10
  super
9
11
  end
10
12
  end
11
13
 
12
14
  #
13
15
  def evaluate(line, line_no, exception: nil)
14
- Console1984.supervisor.execute_supervised(Array(line)) do
16
+ Console1984.command_executor.execute(Array(line)) do
15
17
  super
16
18
  end
17
19
  end
18
-
19
- include Console1984::Freezeable
20
20
  end
@@ -1,5 +1,5 @@
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
3
  include Console1984::Freezeable
4
4
 
5
5
  def write(*args)
@@ -28,7 +28,7 @@ module Console1984::ProtectedTcpSocket
28
28
  end
29
29
 
30
30
  def protected_addresses
31
- @protected_addresses ||= Console1984.currently_protected_urls.collect do |url|
31
+ @protected_addresses ||= Console1984::Shield::Modes::PROTECTED_MODE.currently_protected_urls.collect do |url|
32
32
  host, port = host_and_port_from(url)
33
33
  Array(Addrinfo.getaddrinfo(host, port)).collect { |addrinfo| ComparableAddress.new(addrinfo) if addrinfo.ip_address }
34
34
  end.flatten.compact.uniq
@@ -1,7 +1,17 @@
1
- # Prevents adding new methods to classes.
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.
2
4
  #
3
- # This prevents manipulating certain Console1984 classes
4
- # during a console session.
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.
5
15
  module Console1984::Freezeable
6
16
  extend ActiveSupport::Concern
7
17
 
@@ -12,7 +22,7 @@ module Console1984::Freezeable
12
22
  end
13
23
 
14
24
  class_methods do
15
- SENSITIVE_INSTANCE_METHODS = %i[ instance_variable_set ]
25
+ SENSITIVE_INSTANCE_METHODS = %i[ instance_variable_get instance_variable_set ]
16
26
 
17
27
  def prevent_sensitive_overrides
18
28
  SENSITIVE_INSTANCE_METHODS.each do |method|
@@ -23,7 +33,7 @@ module Console1984::Freezeable
23
33
  private
24
34
  def prevent_sensitive_method(method_name)
25
35
  define_method method_name do |*arguments|
26
- raise Console1984::Errors::ForbiddenCodeManipulation, "You can't invoke #{method_name} on #{self}"
36
+ raise Console1984::Errors::ForbiddenCommand, "You can't invoke #{method_name} on #{self}"
27
37
  end
28
38
  end
29
39
  end
@@ -1,4 +1,4 @@
1
- module Console1984::Supervisor::InputOutput
1
+ module Console1984::InputOutput
2
2
  include Console1984::Freezeable, Console1984::Messages
3
3
 
4
4
  private
@@ -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,27 @@
1
+ # An execution mode that protects encrypted information and connection to external systems.
2
+ class Console1984::Shield::Modes::Protected
3
+ include Console1984::Freezeable
4
+
5
+ delegate :protected_urls, to: Console1984
6
+
7
+ thread_mattr_accessor :currently_protected_urls, default: []
8
+
9
+ def execute(&block)
10
+ protecting(&block)
11
+ end
12
+
13
+ private
14
+ def protecting(&block)
15
+ protecting_connections do
16
+ ActiveRecord::Encryption.protecting_encrypted_data(&block)
17
+ end
18
+ end
19
+
20
+ def protecting_connections
21
+ old_currently_protected_urls = self.currently_protected_urls
22
+ self.currently_protected_urls = protected_urls
23
+ yield
24
+ ensure
25
+ self.currently_protected_urls = old_currently_protected_urls
26
+ end
27
+ end
@@ -0,0 +1,8 @@
1
+ # An execution mode that doesn't protect encrypted information or external systems.
2
+ class Console1984::Shield::Modes::Unprotected
3
+ include Console1984::Freezeable
4
+
5
+ def execute(&block)
6
+ block.call
7
+ end
8
+ end
@@ -0,0 +1,60 @@
1
+ # Console 1984 operates in two modes:
2
+ #
3
+ # * Protected: it won't reveal encrypted information, attempt to connect to protected urls will be prevented.
4
+ # * Unprotected: it will reveal encrypted information and let all connections go through.
5
+ #
6
+ # Tampering attempts (such as deleting audit trails) is prevented in both modes.
7
+ module Console1984::Shield::Modes
8
+ include Console1984::Messages, Console1984::InputOutput
9
+ include Console1984::Freezeable
10
+
11
+ PROTECTED_MODE = Protected.new
12
+ UNPROTECTED_MODE = Unprotected.new
13
+
14
+ # Switch to protected mode
15
+ #
16
+ # Pass +silent: true+ to hide an informative message when switching to this mode.
17
+ def enable_unprotected_mode(silent: false)
18
+ command_executor.run_as_system do
19
+ show_warning Console1984.enter_unprotected_encryption_mode_warning if !silent && protected_mode?
20
+ 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)?"
21
+ session_logger.start_sensitive_access justification
22
+ nil
23
+ end
24
+ ensure
25
+ @mode = UNPROTECTED_MODE
26
+ nil
27
+ end
28
+
29
+ # Switch to unprotected mode
30
+ #
31
+ # Pass +silent: true+ to hide an informative message when switching to this mode.
32
+ def enable_protected_mode(silent: false)
33
+ command_executor.run_as_system do
34
+ show_warning Console1984.enter_protected_mode_warning if !silent && unprotected_mode?
35
+ session_logger.end_sensitive_access
36
+ nil
37
+ end
38
+ ensure
39
+ @mode = PROTECTED_MODE
40
+ nil
41
+ end
42
+
43
+ # Executes the passed block in the configured mode (protected or unprotected).
44
+ def with_protected_mode(&block)
45
+ @mode.execute(&block)
46
+ end
47
+
48
+ def unprotected_mode?
49
+ @mode.is_a?(Unprotected)
50
+ end
51
+
52
+ def protected_mode?
53
+ !unprotected_mode?
54
+ end
55
+
56
+ private
57
+ def current_username
58
+ username_resolver.current
59
+ end
60
+ end
@@ -0,0 +1,86 @@
1
+ # The shield implements the protection mechanisms while using the console:
2
+ #
3
+ # * It extends different systems with console1984 extensions (including IRB itself).
4
+ # * It offers an API to the rest of the system to enable and disable protected modes and
5
+ # execute code on the configured mode.
6
+ #
7
+ # Protection happens at two levels:
8
+ #
9
+ # * External: preventing users from accessing encrypted data or protected systems while on
10
+ # protected mode.
11
+ # * Internal: preventing users from tampering Console 1984 itself.
12
+ class Console1984::Shield
13
+ include Modes
14
+ include Console1984::Freezeable
15
+
16
+ delegate :username_resolver, :session_logger, :command_executor, to: Console1984
17
+
18
+ # Installs the shield by extending several systems and freezing classes and modules
19
+ # that aren't mean to be modified once the console is running.
20
+ def install
21
+ extend_protected_systems
22
+ freeze_all
23
+ end
24
+
25
+ private
26
+ def extend_protected_systems
27
+ extend_irb
28
+ extend_core_ruby
29
+ extend_sockets
30
+ extend_active_record
31
+ end
32
+
33
+ def extend_irb
34
+ IRB::Context.prepend(Console1984::Ext::Irb::Context)
35
+ Rails::ConsoleMethods.include(Console1984::Ext::Irb::Commands)
36
+ end
37
+
38
+ def extend_core_ruby
39
+ Object.prepend Console1984::Ext::Core::Object
40
+ end
41
+
42
+ def extend_sockets
43
+ socket_classes = [TCPSocket, OpenSSL::SSL::SSLSocket]
44
+ OpenSSL::SSL::SSLSocket.include(SSLSocketRemoteAddress)
45
+
46
+ if defined?(Redis::Connection)
47
+ socket_classes.push(*[Redis::Connection::TCPSocket, Redis::Connection::SSLSocket])
48
+ end
49
+
50
+ socket_classes.compact.each do |socket_klass|
51
+ socket_klass.prepend Console1984::Ext::Socket::TcpSocket
52
+ socket_klass.freeze
53
+ end
54
+ end
55
+
56
+ ACTIVE_RECORD_CONNECTION_ADAPTERS = %w[ActiveRecord::ConnectionAdapters::Mysql2Adapter ActiveRecord::ConnectionAdapters::PostgreSQLAdapter ActiveRecord::ConnectionAdapters::SQLite3Adapter]
57
+
58
+ def extend_active_record
59
+ ACTIVE_RECORD_CONNECTION_ADAPTERS.each do |class_string|
60
+ if Object.const_defined?(class_string)
61
+ klass = class_string.constantize
62
+ klass.prepend(Console1984::Ext::ActiveRecord::ProtectedAuditableTables)
63
+ klass.include(Console1984::Freezeable)
64
+ end
65
+ end
66
+ end
67
+
68
+ def freeze_all
69
+ eager_load_all_classes
70
+ Console1984.config.freeze unless Console1984.config.test_mode
71
+ Console1984::Freezeable.freeze_all
72
+ Parser::CurrentRuby.freeze
73
+ end
74
+
75
+ def eager_load_all_classes
76
+ Rails.application.eager_load! unless Rails.application.config.eager_load
77
+ Console1984.class_loader.eager_load
78
+ end
79
+
80
+ module SSLSocketRemoteAddress
81
+ # Serve remote address as TCPSocket so that our extension works with both.
82
+ def remote_address
83
+ Addrinfo.getaddrinfo(hostname, 443).first
84
+ end
85
+ end
86
+ end
@@ -1,31 +1,43 @@
1
- require 'colorized_string'
2
1
  require 'rails/console/app'
3
2
 
4
- # Protects console sessions and executes code in supervised mode.
3
+ # Entry point to the system. In charge of installing everything
4
+ # and starting and stopping sessions.
5
5
  class Console1984::Supervisor
6
- include Accesses, Console1984::Freezeable, Executor, InputOutput, Protector
6
+ include Console1984::Freezeable, Console1984::InputOutput
7
7
 
8
+ delegate :username_resolver, :session_logger, :shield, to: Console1984
9
+
10
+ # Installs the console protections.
11
+ #
12
+ # See Console1984::Shield
8
13
  def install
9
- extend_protected_systems
10
- freeze_all
14
+ require_dependencies
15
+ shield.install
11
16
  end
12
17
 
13
- # Starts a console session extending IRB and several systems to inject
14
- # the protection logic, and notifies the session logger to record the
15
- # session.
18
+ # Starts a console session.
19
+ #
20
+ # This will enable protected mode and log the new session in the configured
21
+ # {session logger}[rdoc-ref:Console1984::SessionsLogger::Database].
16
22
  def start
17
- disable_access_to_encrypted_content(silent: true)
18
-
23
+ shield.enable_protected_mode(silent: true)
19
24
  show_welcome_message
20
-
21
25
  start_session
22
26
  end
23
27
 
28
+ # Stops a console session
24
29
  def stop
25
30
  stop_session
26
31
  end
27
32
 
28
33
  private
34
+ def require_dependencies
35
+ Kernel.silence_warnings do
36
+ require 'parser/current'
37
+ end
38
+ require 'colorized_string'
39
+ end
40
+
29
41
  def start_session
30
42
  session_logger.start_session current_username, ask_for_session_reason
31
43
  end
@@ -34,28 +46,7 @@ class Console1984::Supervisor
34
46
  session_logger.finish_session
35
47
  end
36
48
 
37
- def freeze_all
38
- eager_load_all_classes
39
- Console1984.config.freeze unless Console1984.config.test_mode
40
- Console1984::Freezeable.freeze_all
41
- end
42
-
43
- def eager_load_all_classes
44
- Rails.application.eager_load! unless Rails.application.config.eager_load
45
- Console1984.class_loader.eager_load
46
- end
47
-
48
- def session_logger
49
- Console1984.session_logger
50
- end
51
-
52
49
  def current_username
53
- Console1984.username_resolver.current
50
+ username_resolver.current
54
51
  end
55
-
56
- def username_resolver
57
- Console1984.username_resolver
58
- end
59
-
60
- include Console1984::Freezeable
61
52
  end