console1984 0.2.3 → 0.2.5
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.
- checksums.yaml +4 -4
- data/README.md +8 -0
- data/lib/console1984/command_validator/.command_parser.rb +4 -4
- data/lib/console1984/command_validator/parsed_command.rb +1 -3
- data/lib/console1984/engine.rb +4 -0
- data/lib/console1984/ext/active_record/protected_auditable_tables.rb +9 -1
- data/lib/console1984/query_auditor.rb +57 -0
- data/lib/console1984/refrigerator.rb +1 -1
- data/lib/console1984/supervisor.rb +0 -6
- data/lib/console1984/version.rb +1 -1
- data/lib/console1984.rb +22 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 315c8962dd29be4689059dfd02352fc55a0a30ad0d00de1267f033886156084f
|
|
4
|
+
data.tar.gz: 7f3ee9d6b9a3bebb3616d8ea1dadb7d2de40bea1141c2763a4f46fcffb89c532
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d127676bd46058d5128a139509055f77561e1f4abbf56f8b52730cc5d2c9190e5dfe5ed1c6f6e91cafc1f8ef7b4b60b200e98f5969dbd2f2b374685d026274c7
|
|
7
|
+
data.tar.gz: 72213cf757e348f7e5b29ce5d515d38e5b0d4e0e5694491d02dcf120cab4596f434f0baef9c4850f7b010221b781cd47ad12c4813b26a370c1408059e45fefd8
|
data/README.md
CHANGED
|
@@ -60,6 +60,14 @@ Unnamed, why are you using this console today?
|
|
|
60
60
|
> ...
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
+
`rails query` doesn't prompt, so each query is recorded as its own session with the reason `rails query`, or `rails query (via <agent>)` when a known agent's environment variable is set, such as `rails query (via Claude Code)`. To record why a query was run, set `CONSOLE_REASON`, e.g. to the card, issue or support conversation that authorized it:
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
$ CONSOLE_REASON=https://app.basecamp.com/1/buckets/2/card_tables/cards/3 rails query 'Account.count'
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The session is recorded with the reason `rails query: https://app.basecamp.com/…`. The value is flattened to one line of plain text and capped at 255 characters.
|
|
70
|
+
|
|
63
71
|
### Auditing sessions
|
|
64
72
|
|
|
65
73
|
Check out [`audits1984`](https://github.com/basecamp/audits1984), a companion auditing tool prepared to work with `console1984` database session trails.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Naming class with dot so that it doesn't get loaded eagerly by Zeitwerk. We want to load
|
|
2
|
-
# only when a console session is started
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
# only when a console session is started.
|
|
3
|
+
Console1984.require_ruby_parser_dependencies
|
|
4
|
+
|
|
5
5
|
class Console1984::CommandValidator::CommandParser < ::Parser::AST::Processor
|
|
6
6
|
include AST::Processor::Mixin
|
|
7
7
|
include Console1984::Freezeable
|
|
@@ -56,7 +56,7 @@ class Console1984::CommandValidator::CommandParser < ::Parser::AST::Processor
|
|
|
56
56
|
|
|
57
57
|
def on_casgn(node)
|
|
58
58
|
super
|
|
59
|
-
|
|
59
|
+
_, _, value_node = *node
|
|
60
60
|
@constant_assignments.push(*extract_constants(value_node))
|
|
61
61
|
end
|
|
62
62
|
|
|
@@ -1,6 +1,4 @@
|
|
|
1
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
2
|
class Console1984::CommandValidator::ParsedCommand
|
|
5
3
|
include Console1984::Freezeable
|
|
6
4
|
|
|
@@ -15,7 +13,7 @@ class Console1984::CommandValidator::ParsedCommand
|
|
|
15
13
|
private
|
|
16
14
|
def command_parser
|
|
17
15
|
@command_parser ||= Console1984::CommandValidator::CommandParser.new.tap do |processor|
|
|
18
|
-
ast =
|
|
16
|
+
ast = Console1984.ruby_parser.parse(raw_command)
|
|
19
17
|
processor.process(ast)
|
|
20
18
|
rescue Parser::SyntaxError
|
|
21
19
|
# Fail open with syntax errors
|
data/lib/console1984/engine.rb
CHANGED
|
@@ -5,7 +5,7 @@ module Console1984::Ext::ActiveRecord::ProtectedAuditableTables
|
|
|
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.command_executor.executing_user_command? && sql
|
|
8
|
+
if Console1984.command_executor.executing_user_command? && auditable_sql(sql) =~ auditable_tables_regexp
|
|
9
9
|
raise Console1984::Errors::ForbiddenCommandAttempted, "#{sql}"
|
|
10
10
|
else
|
|
11
11
|
super(*args, **kwargs)
|
|
@@ -14,6 +14,14 @@ module Console1984::Ext::ActiveRecord::ProtectedAuditableTables
|
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
private
|
|
17
|
+
# exec_insert_all receives an ActiveRecord::InsertAll, not a SQL string, so
|
|
18
|
+
# #b is undefined on it. Check its target table name instead, so insert_all
|
|
19
|
+
# and upsert_all don't blow up when run from the console.
|
|
20
|
+
def auditable_sql(sql)
|
|
21
|
+
string = sql.is_a?(String) ? sql : (sql.try(:model)&.table_name || sql.to_s)
|
|
22
|
+
string.b
|
|
23
|
+
end
|
|
24
|
+
|
|
17
25
|
def auditable_tables_regexp
|
|
18
26
|
@auditable_tables_regexp ||= Regexp.new("#{auditable_tables.join("|")}")
|
|
19
27
|
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
class Console1984::QueryAuditor
|
|
2
|
+
REASON_ENV_KEY = "CONSOLE_REASON"
|
|
3
|
+
REASON_LENGTH_LIMIT = 255
|
|
4
|
+
|
|
5
|
+
mattr_accessor :known_agents, default: {
|
|
6
|
+
"CLAUDECODE" => "Claude Code",
|
|
7
|
+
"CODEX_THREAD_ID" => "Codex"
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
def self.install
|
|
11
|
+
ActiveSupport::Notifications.subscribe("query.rails", new)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def start(name, id, payload)
|
|
15
|
+
return unless Console1984.running_protected_environment?
|
|
16
|
+
|
|
17
|
+
Console1984.session_logger.start_session(resolved_username, session_reason)
|
|
18
|
+
Console1984.session_logger.before_executing([ payload[:expression].to_s ])
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def finish(name, id, payload)
|
|
22
|
+
return unless Console1984.running_protected_environment?
|
|
23
|
+
|
|
24
|
+
Console1984.session_logger.finish_session
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
def resolved_username
|
|
29
|
+
Console1984.username_resolver.current.presence || "unknown"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def session_reason
|
|
33
|
+
[ default_reason, given_reason ].compact.join(": ")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def default_reason
|
|
37
|
+
if agent = detected_agent
|
|
38
|
+
"rails query (via #{agent})"
|
|
39
|
+
else
|
|
40
|
+
"rails query"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def detected_agent
|
|
45
|
+
known_agents.find { |var, _| ENV[var].present? }&.last
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# The authorization link (card, issue, support conversation) for this query, from CONSOLE_REASON.
|
|
49
|
+
# It is shown in the audit UI, so keep it to one line of plain text: no control or format
|
|
50
|
+
# characters (newlines, bidi overrides), no markup brackets, and a bounded length.
|
|
51
|
+
def given_reason
|
|
52
|
+
ENV.fetch(REASON_ENV_KEY, "").dup.force_encoding(Encoding::UTF_8).scrub("")
|
|
53
|
+
.gsub(/[\p{Cc}\p{Cf}<>]/, " ").squish
|
|
54
|
+
.truncate(REASON_LENGTH_LIMIT, omission: "…")
|
|
55
|
+
.presence
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -27,6 +27,6 @@ class Console1984::Refrigerator
|
|
|
27
27
|
|
|
28
28
|
def external_modules_and_classes_to_freeze
|
|
29
29
|
# Not using a constant because we want this to run lazily (console-dependant dependencies might not be loaded).
|
|
30
|
-
[
|
|
30
|
+
[Console1984.ruby_parser]
|
|
31
31
|
end
|
|
32
32
|
end
|
|
@@ -41,13 +41,7 @@ class Console1984::Supervisor
|
|
|
41
41
|
|
|
42
42
|
private
|
|
43
43
|
def require_dependencies
|
|
44
|
-
Kernel.silence_warnings do
|
|
45
|
-
require 'parser/current'
|
|
46
|
-
end
|
|
47
44
|
require 'rainbow'
|
|
48
|
-
|
|
49
|
-
# Explicit lazy loading because it depends on +parser+, which we want to only load
|
|
50
|
-
# in console sessions.
|
|
51
45
|
require_relative "./command_validator/.command_parser"
|
|
52
46
|
|
|
53
47
|
# This solves a weird class loading error where ActiveRecord dosn't resolve +Relation+ properly.
|
data/lib/console1984/version.rb
CHANGED
data/lib/console1984.rb
CHANGED
|
@@ -55,6 +55,28 @@ module Console1984
|
|
|
55
55
|
def running_protected_environment?
|
|
56
56
|
protected_environments.collect(&:to_sym).include?(Rails.env.to_sym)
|
|
57
57
|
end
|
|
58
|
+
|
|
59
|
+
def require_ruby_parser_dependencies
|
|
60
|
+
if RUBY_VERSION >= "3.3"
|
|
61
|
+
require 'parser'
|
|
62
|
+
require 'prism'
|
|
63
|
+
else
|
|
64
|
+
Kernel.silence_warnings do
|
|
65
|
+
require 'parser/current'
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Returns the parser class used for parsing console commands.
|
|
71
|
+
# Uses Prism on Ruby >= 3.3 for forward-compatibility with Ruby 4.0+.
|
|
72
|
+
# Falls back to the parser gem on older Rubies.
|
|
73
|
+
def ruby_parser
|
|
74
|
+
if RUBY_VERSION >= "3.3"
|
|
75
|
+
Prism::Translation::ParserCurrent
|
|
76
|
+
else
|
|
77
|
+
Parser::CurrentRuby
|
|
78
|
+
end
|
|
79
|
+
end
|
|
58
80
|
end
|
|
59
81
|
end
|
|
60
82
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: console1984
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jorge Manrubia
|
|
@@ -275,6 +275,7 @@ files:
|
|
|
275
275
|
- lib/console1984/input_output.rb
|
|
276
276
|
- lib/console1984/messages.rb
|
|
277
277
|
- lib/console1984/protections_config.rb
|
|
278
|
+
- lib/console1984/query_auditor.rb
|
|
278
279
|
- lib/console1984/refrigerator.rb
|
|
279
280
|
- lib/console1984/sessions_logger/database.rb
|
|
280
281
|
- lib/console1984/shield.rb
|