console1984 0.1.6 → 0.1.7

Sign up to get free protection for your applications and to get access to all the features.
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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0a76f26ca8567b7261e2062a9360216ebca2c230ffb48e4ecb5e5a9970339e99
4
- data.tar.gz: 6a892b894d3274e9567a4fd763d9121690b889b89e7a2e7a2b9f919825e51144
3
+ metadata.gz: 498f37bd3803f7f76e073d91f51e2dc69fc8a00fc575392c3869b2ae4d65d9bf
4
+ data.tar.gz: 1675df589a603042a54e6de8b835e4434f5a112158521379553e9875b25c9eeb
5
5
  SHA512:
6
- metadata.gz: 125c2457aca08f4f1476db595f465a85c1b2d9dfa22634502b398456aeb0816436563372a42a1c2182a731a87b7c8552bb4429b775bd2b6f320fbc86bd74d5b3
7
- data.tar.gz: bd3ed3832febd22d4f6038ee8e3bf2db7471f9bbc62a77bb8934ee0904681050b072b67c12d8bccac3b2eebc8e52c8f06f06c32cf0407be6fb9f0008248f0834
6
+ metadata.gz: aa0edb371ac31cd2b87e1b8ddb833482516bed91ff3a5c85c12c10c2206ce7dbe8aba514a6a3aecc2e5d594eee527db78b270cd0789484960c7c98e55ca504aa
7
+ data.tar.gz: bc595388d7ca433c22a1ccc17a5bb28759cc0b0dd8b5267a061456ad0e17de58be0f95a129689e7fbe88915c709af82d0efd70a3f410f4b40cb0f7e767fa7cb6
data/README.md CHANGED
@@ -155,4 +155,17 @@ These config options are namespaced in `config.console1984`:
155
155
 
156
156
  `console1984` uses Ruby to add several protection mechanisms. However, because Ruby is highly dynamic, it's technically possible to circumvent most of these controls if you know what you are doing. We have made an effort to prevent such attempts, but if your organization needs bullet-proof protection against malicious actors using the console, you should consider additional security measures.
157
157
 
158
- The current version includes protection mechanisms to avoid tampering the tables that store console sessions. A definitive mechanism to do this would be using a read only connection when user commands are evaluated. Implementing such scheme is possible by writing a custom session logger and leveraging Rails' multi-database support. We would like that future versions of `console1984` supported this scheme directly as a configuration option.
158
+ The current version includes protection mechanisms to avoid tampering the tables that store console sessions. A bullet-proof mechanism would be using a read only connection when user commands are evaluated. Implementing such scheme is possible by writing a custom session logger and leveraging Rails' multi-database support. We would like that future versions of `console1984` supported this scheme directly as a configuration option.
159
+
160
+ ## Running the test suite
161
+
162
+ The test suite runs against SQLite by default, but can be run against Postgres and MySQL too. It will run against the three in the CI server.
163
+
164
+ To run the suite in your computer, first, run `bin/setup` to create the docker containers for MySQL/PostgreSQL and create the databases. Then run:
165
+
166
+ ```bash
167
+ bin/rails test # against SQLite (default)
168
+ bin/rails test TARGET_DB=mysql
169
+ bin/rails test TARGET_DB=postgres
170
+ bin/rails test TARGET_DB=sqlite
171
+ ```
@@ -0,0 +1,17 @@
1
+ forbidden_reopening:
2
+ - ActiveRecord
3
+ - Console1984
4
+ - PG
5
+ - Mysql2
6
+ forbidden_constant_reference:
7
+ always:
8
+ - Console1984
9
+ protected:
10
+ - PG
11
+ - Mysql2
12
+ - ActiveRecord::ActiveRecordEncryption
13
+ suspicious_terms:
14
+ - console_1984
15
+ - Console1984
16
+ - secret
17
+ - credentials
@@ -0,0 +1,94 @@
1
+ # Supervise execution of console commands:
2
+ #
3
+ # * It will {validate commands}[rdoc-ref:Console1984::CommandValidator] before running
4
+ # them.
5
+ # * It will execute the commands in {protected mode}[rdoc-ref:Console1984::Shield#with_protected_mode]
6
+ # if needed.
7
+ # * It will log the command execution, and flag suspicious attempts and forbidden commands
8
+ # appropriately.
9
+ class Console1984::CommandExecutor
10
+ include Console1984::Freezeable
11
+
12
+ delegate :username_resolver, :session_logger, :shield, to: Console1984
13
+
14
+ # Logs and validates +commands+, and executes the passed block in a protected environment.
15
+ #
16
+ # Suspicious commands will be executed but flagged as suspicious. Forbidden commands will
17
+ # be prevented and flagged too.
18
+ def execute(commands, &block)
19
+ run_as_system { session_logger.before_executing commands }
20
+ validate_command commands
21
+ execute_in_protected_mode(&block)
22
+ rescue Console1984::Errors::ForbiddenCommand, FrozenError
23
+ flag_suspicious(commands)
24
+ rescue Console1984::Errors::SuspiciousCommand
25
+ flag_suspicious(commands)
26
+ execute_in_protected_mode(&block)
27
+ rescue FrozenError
28
+ flag_suspicious(commands)
29
+ ensure
30
+ run_as_system { session_logger.after_executing commands }
31
+ end
32
+
33
+ # Executes the passed block in protected mode.
34
+ #
35
+ # See Console1984::Shield::Modes.
36
+ def execute_in_protected_mode(&block)
37
+ run_as_user do
38
+ shield.with_protected_mode(&block)
39
+ end
40
+ end
41
+
42
+ # Executes the passed block as a user.
43
+ #
44
+ # While the block is being executed, #executing_user_command? will return true.
45
+ # This method helps implementing certain protection mechanisms that should only act with
46
+ # user commands.
47
+ def run_as_user(&block)
48
+ run_command true, &block
49
+ end
50
+
51
+ # Executes the passed block as the system.
52
+ #
53
+ # While the block is being executed, #executing_user_command? will return false.
54
+ def run_as_system(&block)
55
+ run_command false, &block
56
+ end
57
+
58
+ # Returns whether the system is currently executing a user command.
59
+ def executing_user_command?
60
+ @executing_user_command
61
+ end
62
+
63
+ # Validates the command.
64
+ #
65
+ # See Console1984::CommandValidator.
66
+ def validate_command(command)
67
+ command_validator.validate(command)
68
+ end
69
+
70
+ private
71
+ COMMAND_VALIDATOR_CONFIG_FILE_PATH = Console1984::Engine.root.join("config/command_protections.yml")
72
+
73
+ def command_validator
74
+ @command_validator ||= build_command_validator
75
+ end
76
+
77
+ def build_command_validator
78
+ Console1984::CommandValidator.from_config(YAML.safe_load(File.read(COMMAND_VALIDATOR_CONFIG_FILE_PATH)).symbolize_keys)
79
+ end
80
+
81
+ def flag_suspicious(commands)
82
+ puts "Forbidden command attempted: #{commands.join("\n")}"
83
+ run_as_system { session_logger.suspicious_commands_attempted commands }
84
+ nil
85
+ end
86
+
87
+ def run_command(run_by_user, &block)
88
+ original_value = @executing_user_command
89
+ @executing_user_command = run_by_user
90
+ block.call
91
+ ensure
92
+ @executing_user_command = original_value
93
+ end
94
+ end
@@ -0,0 +1,31 @@
1
+ # Validates references to a configured set of constants.
2
+ class Console1984::CommandValidator::ForbiddenConstantReferenceValidation
3
+ include Console1984::Freezeable
4
+
5
+ # +config+ will be a hash like:
6
+ #
7
+ # { always: [ Console1984 ], protected: [ PG, Mysql2 ] }
8
+ def initialize(shield = Console1984.shield, config)
9
+ # We make shield an injectable dependency for testing purposes. Everything is frozen
10
+ # for security purposes, so stubbing won't work.
11
+ @shield = shield
12
+
13
+ @forbidden_constants_names = config[:always] || []
14
+ @constant_names_forbidden_in_protected_mode = config[:protected] || []
15
+ end
16
+
17
+ # Raises a Console1984::Errors::ForbiddenCommand if a banned constant is referenced.
18
+ def validate(parsed_command)
19
+ if contains_invalid_const_reference?(parsed_command, @forbidden_constants_names) ||
20
+ (@shield.protected_mode? && contains_invalid_const_reference?(parsed_command, @constant_names_forbidden_in_protected_mode))
21
+ raise Console1984::Errors::ForbiddenCommand
22
+ end
23
+ end
24
+
25
+ private
26
+ def contains_invalid_const_reference?(parsed_command, banned_constants)
27
+ parsed_command.constants.find do |constant_name|
28
+ banned_constants.find { |banned_constant| "#{constant_name}::".start_with?("#{banned_constant}::") }
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,29 @@
1
+ # Validates attempts to reopen classes and modules based on a configured set.
2
+ class Console1984::CommandValidator::ForbiddenReopeningValidation
3
+ include Console1984::Freezeable
4
+
5
+ attr_reader :banned_class_or_module_names
6
+
7
+ def initialize(banned_classes_or_modules)
8
+ @banned_class_or_module_names = banned_classes_or_modules.collect(&:to_s)
9
+ end
10
+
11
+ # Raises a Console1984::Errors::ForbiddenCommand if an banned class or module reopening
12
+ # is detected.
13
+ def validate(parsed_command)
14
+ if contains_invalid_class_or_module_declaration?(parsed_command)
15
+ raise Console1984::Errors::ForbiddenCommand
16
+ end
17
+ end
18
+
19
+ private
20
+ def contains_invalid_class_or_module_declaration?(parsed_command)
21
+ parsed_command.declared_classes_or_modules.find { |class_or_module_name| banned?(class_or_module_name) }
22
+ end
23
+
24
+ def banned?(class_or_module_name)
25
+ @banned_class_or_module_names.find do |banned_class_or_module_name|
26
+ "#{class_or_module_name}::".start_with?("#{banned_class_or_module_name}::")
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,64 @@
1
+ # Parses a command string and exposes different constructs to be used by validations.
2
+ #
3
+ # Internally, it uses the {parser}[https://github.com/whitequark/parser] gem to perform the parsing.
4
+ class Console1984::CommandValidator::ParsedCommand
5
+ include Console1984::Freezeable
6
+
7
+ attr_reader :raw_command
8
+
9
+ delegate :declared_classes_or_modules, :constants, to: :processed_ast
10
+
11
+ def initialize(raw_command)
12
+ @raw_command = Array(raw_command).join("\n")
13
+ end
14
+
15
+ private
16
+ def processed_ast
17
+ @processed_ast ||= CommandProcessor.new.tap do |processor|
18
+ ast = Parser::CurrentRuby.parse(raw_command)
19
+ processor.process(ast)
20
+ rescue Parser::SyntaxError
21
+ # Fail open with syntax errors
22
+ end
23
+ end
24
+
25
+ class CommandProcessor < ::Parser::AST::Processor
26
+ include AST::Processor::Mixin
27
+ include Console1984::Freezeable
28
+
29
+ attr_reader :constants, :declared_classes_or_modules
30
+
31
+ def initialize
32
+ @constants = []
33
+ @declared_classes_or_modules = []
34
+ end
35
+
36
+ def on_class(node)
37
+ super
38
+ const_declaration, _, _ = *node
39
+
40
+ processor = self.class.new
41
+ processor.process(const_declaration)
42
+ @declared_classes_or_modules << processor.constants.first if processor.constants.present?
43
+ end
44
+
45
+ alias_method :on_module, :on_class
46
+
47
+ def on_const(node)
48
+ super
49
+ name, const_name = *node
50
+ const_name = const_name.to_s
51
+ last_constant = @constants.last
52
+
53
+ if name.nil? || (name && name.type == :cbase) # cbase = leading ::
54
+ if last_constant&.end_with?("::")
55
+ last_constant << const_name
56
+ else
57
+ @constants << const_name
58
+ end
59
+ elsif last_constant
60
+ last_constant << "::#{const_name}"
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,22 @@
1
+ # Validates that the command doesn't include a term based on a configured list.
2
+ class Console1984::CommandValidator::SuspiciousTermsValidation
3
+ include Console1984::Freezeable
4
+
5
+ def initialize(suspicious_terms)
6
+ @suspicious_terms = suspicious_terms
7
+ end
8
+
9
+ # Raises a Console1984::Errors::SuspiciousCommand if the term is referenced.
10
+ def validate(parsed_command)
11
+ if contains_suspicious_term?(parsed_command)
12
+ raise Console1984::Errors::SuspiciousCommand
13
+ end
14
+ end
15
+
16
+ private
17
+ def contains_suspicious_term?(parsed_command)
18
+ @suspicious_terms.find do |term|
19
+ parsed_command.raw_command.include?(term)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,71 @@
1
+ # Validates console commands.
2
+ #
3
+ # This performs an static analysis of console commands. The analysis is meant to happen
4
+ # *before* commands are executed, so that they can prevent the execution if needed.
5
+ #
6
+ # The validation itself happens as a chain of validation objects. The system will invoke
7
+ # each validation in order. Validations will raise an error if the validation fails (typically
8
+ # a Console1984::Errors::ForbiddenCommand or Console1984::Errors::SuspiciousCommands).
9
+ #
10
+ # Internally, validations will receive a Console1984::CommandValidator::ParsedCommand object. This
11
+ # exposes parsed constructs in addition to the raw strings so that validations can use those.
12
+ #
13
+ # There is a convenience method .from_config that lets you instantiate a validation setup from
14
+ # a config hash (e.g to customize validations via YAML).
15
+ #
16
+ # See +config/command_protections.yml+ and the validations in +lib/console1984/command_validator+.
17
+ class Console1984::CommandValidator
18
+ include Console1984::Freezeable
19
+
20
+ def initialize
21
+ @validations_by_name = HashWithIndifferentAccess.new
22
+ end
23
+
24
+ class << self
25
+ # Instantiates a command validator that will configure the validations based on the config passed.
26
+ #
27
+ # For each key in +config+, it will derive the class Console1984::CommandValidator::#{key.camelize}Validation
28
+ # and will instantiate the validation passed the values as params.
29
+ #
30
+ # For example for this config:
31
+ #
32
+ # { forbidden_reopening: [ActiveRecord, Console1984] }
33
+ #
34
+ # It will instantiate Console1984::CommandValidator::ForbiddenReopeningValidation passing
35
+ # +["ActiveRecord", "Console1984"]+ in the constructor.
36
+ #
37
+ # # See +config/command_protections.yml+ as an example.
38
+ def from_config(config)
39
+ Console1984::CommandValidator.new.tap do |validator|
40
+ config.each do |validator_name, validator_config|
41
+ validator_class = "Console1984::CommandValidator::#{validator_name.to_s.camelize}Validation".constantize
42
+ validator_config.try(:symbolize_keys!)
43
+ validator.add_validation validator_name, validator_class.new(validator_config)
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ # Adds a +validation+ to the chain indexed by the provided +name+
50
+ #
51
+ # Validations are executed in the order they are added.
52
+ def add_validation(name, validation)
53
+ validations_by_name[name] = validation
54
+ end
55
+
56
+ # Executes the chain of validations passing a {parsed command}[rdoc-ref:Console1984::CommandValidator::ParsedCommand]
57
+ # created with the +command+ string passed by parameter.
58
+ #
59
+ # The validations are executed in the order they were added. If one validation raises an error, the error will
60
+ # raise and the rest of validations won't get checked.
61
+ def validate(command)
62
+ parsed_command = ParsedCommand.new(command)
63
+
64
+ validations_by_name.values.each do |validation|
65
+ validation.validate(parsed_command)
66
+ end
67
+ end
68
+
69
+ private
70
+ attr_reader :validations_by_name
71
+ end
@@ -1,9 +1,11 @@
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
5
  include Console1984::Freezeable, Console1984::Messages
4
6
 
5
7
  PROPERTIES = %i[
6
- session_logger username_resolver
8
+ session_logger username_resolver shield command_executor
7
9
  protected_environments protected_urls
8
10
  production_data_warning enter_unprotected_encryption_mode_warning enter_protected_mode_warning
9
11
  incinerate incinerate_after incineration_queue
@@ -24,16 +26,18 @@ class Console1984::Config
24
26
 
25
27
  def freeze
26
28
  super
27
- protected_urls.freeze
29
+ [ protected_urls ].each(&:freeze)
28
30
  end
29
31
 
30
32
  private
31
33
  def set_defaults
32
- self.protected_environments = []
33
- self.protected_urls = []
34
-
35
34
  self.session_logger = Console1984::SessionsLogger::Database.new
36
35
  self.username_resolver = Console1984::Username::EnvResolver.new("CONSOLE_USER")
36
+ self.shield = Console1984::Shield.new
37
+ self.command_executor = Console1984::CommandExecutor.new
38
+
39
+ self.protected_environments = []
40
+ self.protected_urls = []
37
41
 
38
42
  self.production_data_warning = DEFAULT_PRODUCTION_DATA_WARNING
39
43
  self.enter_unprotected_encryption_mode_warning = DEFAULT_ENTER_UNPROTECTED_ENCRYPTION_MODE_WARNING
@@ -14,6 +14,8 @@ module Console1984
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
 
@@ -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 ForbiddenCodeManipulation < StandardError; end
13
22
  end
14
23
  end
@@ -1,11 +1,11 @@
1
1
  # Prevents accessing trail model tables when executing console commands.
2
- module Console1984::ProtectedAuditableTables
2
+ module Console1984::Ext::ActiveRecord::ProtectedAuditableTables
3
3
  include Console1984::Freezeable
4
4
 
5
5
  %i[ execute exec_query exec_insert exec_delete exec_update exec_insert_all ].each do |method|
6
6
  define_method method do |*args, **kwargs|
7
7
  sql = args.first
8
- if Console1984.supervisor.executing_user_command? && sql =~ auditable_tables_regexp
8
+ if Console1984.command_executor.executing_user_command? && sql =~ auditable_tables_regexp
9
9
  raise Console1984::Errors::ForbiddenCommand, "#{sql}"
10
10
  else
11
11
  super(*args, **kwargs)
@@ -25,6 +25,4 @@ module Console1984::ProtectedAuditableTables
25
25
  def auditable_models
26
26
  @auditable_models ||= Console1984::Base.descendants
27
27
  end
28
-
29
- include Console1984::Freezeable
30
28
  end