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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3bcb1c5436456f551d695a6b3ffdf284959846303adf6afaaee8fe0ff66ecb6e
4
- data.tar.gz: 317f00e5e0c25b8f2e18876782872cc7288df2038c25139538c98c1e77dbeb85
3
+ metadata.gz: 315c8962dd29be4689059dfd02352fc55a0a30ad0d00de1267f033886156084f
4
+ data.tar.gz: 7f3ee9d6b9a3bebb3616d8ea1dadb7d2de40bea1141c2763a4f46fcffb89c532
5
5
  SHA512:
6
- metadata.gz: 0a9fc5c6bb7c6c36580dfae29f4cfaadbc18930d11061a77609bbec85c9a3b44131e873bc347e04f7db2b8ce7dc2d239c9d18a74cedcde14dcb28858697e7c86
7
- data.tar.gz: fb53e1d21f930e80b4d05a77a2e3b2eeed5eb7af22fd242874452cce10b141e65e4f4b7eda3a22a2d52335d7aeed923d22c6732905587f0890d1cfcb630cd78d
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, when +parser+ is loaded.
3
- #
4
- # See +Console1984::Supervisor#require_dependencies+
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
- scope_node, name, value_node = *node
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 = Parser::CurrentRuby.parse(raw_command)
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
@@ -24,5 +24,9 @@ module Console1984
24
24
  Console1984.supervisor.start
25
25
  end
26
26
  end
27
+
28
+ initializer "console1984.query_auditor" do
29
+ Console1984::QueryAuditor.install
30
+ end
27
31
  end
28
32
  end
@@ -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.b =~ auditable_tables_regexp
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
- [Parser::CurrentRuby]
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.
@@ -1,3 +1,3 @@
1
1
  module Console1984
2
- VERSION = "0.2.3"
2
+ VERSION = "0.2.5"
3
3
  end
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.3
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