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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bc64d037f2de5570292e0b09710b4543a68ba1af12759150cc68e7b7f4dd6e16
|
4
|
+
data.tar.gz: c5929af5061393a32c4df38022535d53c62aaaabe951727798e340322fb0d950
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b64f422bcdd421e7a874af965c9ee615f6d873e0d2a685d68667216fd1ee91d4e8e4815d4dd9e78ef7838a75763fb898be1f328c8466a137f1782a4121531da6
|
7
|
+
data.tar.gz: ce7e1f60bfdb666abe3c85a02ddaa1ac25344c994a1471ec64d07cd15f8aac5735bcd0608497e71d9813cf111cdb76f414a2373eb8dbef14b983757ba3876812
|
data/README.md
CHANGED
@@ -10,10 +10,12 @@ A Rails console extension that protects sensitive accesses and makes them audita
|
|
10
10
|
|
11
11
|
If you are looking for the auditing tool, check [`audits1984`](https://github.com/basecamp/audits1984).
|
12
12
|
|
13
|
-
![
|
13
|
+
![Terminal screenshot showing console1984 asking for a reason for the session](docs/images/console-session-reason.png)
|
14
14
|
|
15
15
|
## Installation
|
16
16
|
|
17
|
+
**Important:** `console1984` depends on [Active Record encryption](https://edgeguides.rubyonrails.org/active_record_encryption.html) which is a Rails 7 feature. Since no gem for Rails 7 has been released yet, you need to run Rails edge in your project (point the gem to latest `main` in the [repo](https://github.com/rails/rails)).
|
18
|
+
|
17
19
|
Add it to your `Gemfile`:
|
18
20
|
|
19
21
|
```ruby
|
@@ -69,7 +71,7 @@ To decrypt data, enter the command `decrypt!`. It will ask for a justification,
|
|
69
71
|
irb(main)> Topic.last.name
|
70
72
|
Topic Load (1.4ms) SELECT `topics`.* FROM `topics` ORDER BY `topics`.`id` DESC LIMIT 1
|
71
73
|
=> "{\"p\":\"iu6+LfnNlurC6sL++JyOIDvedjNSz/AvnZQ=\",\"h\":{\"iv\":\"BYa86+JNM/LdkC18\",\"at\":\"r4sQNoSyIlAjJdZEKHVMow==\",\"k\":{\"p\":\"7L1l/5UiYsFQqqo4jfMZtLwp90KqcrIgS7HqgteVjuM=\",\"h\":{\"iv\":\"ItwRYxZAerKIoSZ8\",\"at\":\"ZUSNVfvtm4wAYWLBKRAx/g==\",\"e\":\"QVNDSUktOEJJVA==\"}},\"i\":\"OTdiOQ==\"}}"
|
72
|
-
irb(main)
|
74
|
+
irb(main)> decrypt!
|
73
75
|
```
|
74
76
|
|
75
77
|
```
|
@@ -104,15 +106,11 @@ irb(main)> Topic.last.name
|
|
104
106
|
=> "{\"p\":\"iu6+LfnNlurC6sL++JyOIDvedjNSz/AvnZQ=\",\"h\":{\"iv\":\"BYa86+JNM/LdkC18\",\"at\":\"r4sQNoSyIlAjJdZEKHVMow==\",\"k\":{\"p\":\"7L1l/5UiYsFQqqo4jfMZtLwp90KqcrIgS7HqgteVjuM=\",\"h\":{\"iv\":\"ItwRYxZAerKIoSZ8\",\"at\":\"ZUSNVfvtm4wAYWLBKRAx/g==\",\"e\":\"QVNDSUktOEJJVA==\"}},\"i\":\"OTdiOQ==\"}}"
|
105
107
|
```
|
106
108
|
|
107
|
-
While in protected mode, you can't modify encrypted data, but can save unencrypted attributes normally. If you try to modify an encrypted column it will raise an error
|
108
|
-
|
109
|
-
```ruby
|
110
|
-
irb(main)> Rails.cache.read("some key") # raises Console1984::Errors::ProtectedConnection
|
111
|
-
```
|
109
|
+
While in protected mode, you can't modify encrypted data, but can save unencrypted attributes normally. If you try to modify an encrypted column it will raise an error.
|
112
110
|
|
113
111
|
### Access to external systems
|
114
112
|
|
115
|
-
While Active Record encryption can protect personal information in the database, are other systems can contain very sensitive information. For example: Elasticsearch indexing user information or Redis caching template fragments.
|
113
|
+
While Active Record encryption can protect personal information in the database, there are other systems can contain very sensitive information. For example: Elasticsearch indexing user information or Redis caching template fragments.
|
116
114
|
|
117
115
|
To protect the access to such systems, you can add their URLs to `config.console1984.protected_urls` in the corresponding environment config file (e.g: `production.rb`):
|
118
116
|
|
@@ -120,7 +118,13 @@ To protect the access to such systems, you can add their URLs to `config.console
|
|
120
118
|
config.console1984.protected_urls = [ "https://my-app-us-east-1-whatever.us-east-1.es.amazonaws.com", "redis://my-app-cache-1.whatever.cache.amazonaws.com:6379" ]
|
121
119
|
```
|
122
120
|
|
123
|
-
|
121
|
+
In the default protected mode, trying to read data from a protected system will be aborted with an error:
|
122
|
+
|
123
|
+
```ruby
|
124
|
+
irb(main)> Rails.cache.read("some key") # raises Console1984::Errors::ProtectedConnection
|
125
|
+
```
|
126
|
+
|
127
|
+
Running `decrypt!` will switch you to unprotected mode and let you access these systems normally. The system will ask for a justfication and will flag those accesses as sensitive.
|
124
128
|
|
125
129
|
This will work for systems that use Ruby sockets as the underlying communication mechanism.
|
126
130
|
|
@@ -128,6 +132,10 @@ This will work for systems that use Ruby sockets as the underlying communication
|
|
128
132
|
|
129
133
|
By default, sessions will be incinerated with a job 30 days after they are created. You can configure this period by setting `config.console1984.incinerate_after = 1.year` and you can disable incineration completely by setting `config.console1984.incinerate = false`.
|
130
134
|
|
135
|
+
### Eager loading
|
136
|
+
|
137
|
+
When starting a console session, `console1984` will eager load all the application classes if necessary. In practice, production environments already load classes eagerly, so this won't represent any change for those.
|
138
|
+
|
131
139
|
## Configuration
|
132
140
|
|
133
141
|
These config options are namespaced in `config.console1984`:
|
@@ -137,7 +145,7 @@ These config options are namespaced in `config.console1984`:
|
|
137
145
|
| `protected_environments` | The list of environments where `console1984` will act on. Defaults to `%i[ production ]`. |
|
138
146
|
| `protected_urls` | The list of URLs corresponding with external systems to protect. |
|
139
147
|
| `session_logger` | The system used to record session data. The default logger is `Console1984::SessionsLogger::Database`. |
|
140
|
-
| `username_resolver` | Configure
|
148
|
+
| `username_resolver` | Configure how the current user is determined for a given console session. The default is `Console1984::Username::EnvResolver.new("CONSOLE_USER")`, which returns the value of the environment variable `CONSOLE_USER`. |
|
141
149
|
| `production_data_warning` | The text to show when a console session starts. |
|
142
150
|
| `enter_unprotected_encryption_mode_warning` | The text to show when user enters into unprotected mode. |
|
143
151
|
| `enter_protected_mode_warning` | The text to show when user go backs to protected mode. |
|
@@ -149,3 +157,17 @@ These config options are namespaced in `config.console1984`:
|
|
149
157
|
|
150
158
|
`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.
|
151
159
|
|
160
|
+
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.
|
161
|
+
|
162
|
+
## Running the test suite
|
163
|
+
|
164
|
+
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.
|
165
|
+
|
166
|
+
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:
|
167
|
+
|
168
|
+
```bash
|
169
|
+
bin/rails test # against SQLite (default)
|
170
|
+
bin/rails test TARGET_DB=mysql
|
171
|
+
bin/rails test TARGET_DB=postgres
|
172
|
+
bin/rails test TARGET_DB=sqlite
|
173
|
+
```
|
@@ -0,0 +1,30 @@
|
|
1
|
+
static_validations:
|
2
|
+
forbidden_reopening:
|
3
|
+
- ActiveRecord
|
4
|
+
- Console1984
|
5
|
+
- PG
|
6
|
+
- Mysql2
|
7
|
+
forbidden_constant_reference:
|
8
|
+
always:
|
9
|
+
- Console1984
|
10
|
+
protected:
|
11
|
+
- PG
|
12
|
+
- Mysql2
|
13
|
+
- ActiveRecord::ActiveRecordEncryption
|
14
|
+
suspicious_terms:
|
15
|
+
- console_1984
|
16
|
+
- Console1984
|
17
|
+
- secret
|
18
|
+
- credentials
|
19
|
+
forbidden_methods:
|
20
|
+
always:
|
21
|
+
user:
|
22
|
+
Kernel:
|
23
|
+
- eval
|
24
|
+
Object:
|
25
|
+
- eval
|
26
|
+
BasicObject:
|
27
|
+
- eval
|
28
|
+
- instance_eval
|
29
|
+
Module:
|
30
|
+
- class_eval
|
@@ -0,0 +1,90 @@
|
|
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 => e
|
23
|
+
flag_suspicious(commands)
|
24
|
+
rescue Console1984::Errors::SuspiciousCommand
|
25
|
+
flag_suspicious(commands)
|
26
|
+
execute_in_protected_mode(&block)
|
27
|
+
ensure
|
28
|
+
run_as_system { session_logger.after_executing commands }
|
29
|
+
end
|
30
|
+
|
31
|
+
# Executes the passed block in protected mode.
|
32
|
+
#
|
33
|
+
# See Console1984::Shield::Modes.
|
34
|
+
def execute_in_protected_mode(&block)
|
35
|
+
run_as_user do
|
36
|
+
shield.with_protected_mode(&block)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Executes the passed block as a user.
|
41
|
+
#
|
42
|
+
# While the block is being executed, #executing_user_command? will return true.
|
43
|
+
# This method helps implementing certain protection mechanisms that should only act with
|
44
|
+
# user commands.
|
45
|
+
def run_as_user(&block)
|
46
|
+
run_command true, &block
|
47
|
+
end
|
48
|
+
|
49
|
+
# Executes the passed block as the system.
|
50
|
+
#
|
51
|
+
# While the block is being executed, #executing_user_command? will return false.
|
52
|
+
def run_as_system(&block)
|
53
|
+
run_command false, &block
|
54
|
+
end
|
55
|
+
|
56
|
+
# Returns whether the system is currently executing a user command.
|
57
|
+
def executing_user_command?
|
58
|
+
@executing_user_command
|
59
|
+
end
|
60
|
+
|
61
|
+
# Validates the command.
|
62
|
+
#
|
63
|
+
# See Console1984::CommandValidator.
|
64
|
+
def validate_command(command)
|
65
|
+
command_validator.validate(command)
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
def command_validator
|
70
|
+
@command_validator ||= build_command_validator
|
71
|
+
end
|
72
|
+
|
73
|
+
def build_command_validator
|
74
|
+
Console1984::CommandValidator.from_config(Console1984.protections_config.static_validations)
|
75
|
+
end
|
76
|
+
|
77
|
+
def flag_suspicious(commands)
|
78
|
+
puts "Forbidden command attempted: #{commands.join("\n")}"
|
79
|
+
run_as_system { session_logger.suspicious_commands_attempted commands }
|
80
|
+
nil
|
81
|
+
end
|
82
|
+
|
83
|
+
def run_command(run_by_user, &block)
|
84
|
+
original_value = @executing_user_command
|
85
|
+
@executing_user_command = run_by_user
|
86
|
+
block.call
|
87
|
+
ensure
|
88
|
+
@executing_user_command = original_value
|
89
|
+
end
|
90
|
+
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 + parsed_command.constant_assignments).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 + parsed_command.constant_assignments).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,90 @@
|
|
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, :constant_assignments, 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
|
+
def initialize
|
30
|
+
@constants = []
|
31
|
+
@declared_classes_or_modules = []
|
32
|
+
@constant_assignments = []
|
33
|
+
end
|
34
|
+
|
35
|
+
# We define accessors to define lists without duplicates. We are not using a +SortedSet+ because we want
|
36
|
+
# to mutate strings in the list while the processing is happening. And we don't use metapgroamming to define the
|
37
|
+
# accessors to prevent having problems with freezable and its instance_variable* protection.
|
38
|
+
|
39
|
+
def constants
|
40
|
+
@constants.uniq
|
41
|
+
end
|
42
|
+
|
43
|
+
def declared_classes_or_modules
|
44
|
+
@declared_classes_or_modules.uniq
|
45
|
+
end
|
46
|
+
|
47
|
+
def constant_assignments
|
48
|
+
@constant_assignments.uniq
|
49
|
+
end
|
50
|
+
|
51
|
+
def on_class(node)
|
52
|
+
super
|
53
|
+
const_declaration, _, _ = *node
|
54
|
+
constant = extract_constants(const_declaration).first
|
55
|
+
@declared_classes_or_modules << constant if constant.present?
|
56
|
+
end
|
57
|
+
|
58
|
+
alias_method :on_module, :on_class
|
59
|
+
|
60
|
+
def on_const(node)
|
61
|
+
super
|
62
|
+
name, const_name = *node
|
63
|
+
const_name = const_name.to_s
|
64
|
+
last_constant = @constants.last
|
65
|
+
|
66
|
+
if name.nil? || (name && name.type == :cbase) # cbase = leading ::
|
67
|
+
if last_constant&.end_with?("::")
|
68
|
+
last_constant << const_name
|
69
|
+
else
|
70
|
+
@constants << const_name
|
71
|
+
end
|
72
|
+
elsif last_constant
|
73
|
+
last_constant << "::#{const_name}"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def on_casgn(node)
|
78
|
+
super
|
79
|
+
scope_node, name, value_node = *node
|
80
|
+
@constant_assignments.push(*extract_constants(value_node))
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
def extract_constants(node)
|
85
|
+
self.class.new.tap do |processor|
|
86
|
+
processor.process(node)
|
87
|
+
end.constants
|
88
|
+
end
|
89
|
+
end
|
90
|
+
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
|