dbviewer 0.6.2 → 0.6.3

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.
data/lib/dbviewer.rb CHANGED
@@ -1,7 +1,8 @@
1
1
  require "dbviewer/version"
2
2
  require "dbviewer/configuration"
3
3
  require "dbviewer/engine"
4
- require "dbviewer/sql_validator"
4
+ require "dbviewer/validator"
5
+ require "dbviewer/validator/sql"
5
6
 
6
7
  require "dbviewer/storage/base"
7
8
  require "dbviewer/storage/in_memory_storage"
@@ -11,6 +12,7 @@ require "dbviewer/query/executor"
11
12
  require "dbviewer/query/analyzer"
12
13
  require "dbviewer/query/collection"
13
14
  require "dbviewer/query/logger"
15
+ require "dbviewer/query/notification_subscriber"
14
16
  require "dbviewer/query/parser"
15
17
 
16
18
  require "dbviewer/database/cache_manager"
@@ -51,61 +53,83 @@ module Dbviewer
51
53
 
52
54
  # This class method will be called by the engine when it's appropriate
53
55
  def setup
54
- # Configure the query logger with current configuration settings
56
+ configure_query_logger
57
+ validate_database_connections
58
+ end
59
+
60
+ private
61
+
62
+ # Configure the query logger with current configuration settings
63
+ def configure_query_logger
55
64
  Dbviewer::Query::Logger.configure(
56
65
  enable_query_logging: configuration.enable_query_logging,
57
66
  query_logging_mode: configuration.query_logging_mode
58
67
  )
68
+ end
59
69
 
60
- # Check all configured connections
61
- connection_errors = []
62
- configuration.database_connections.each do |key, config|
63
- begin
64
- connection_class = nil
65
-
66
- if config[:connection]
67
- connection_class = config[:connection]
68
- elsif config[:connection_class].is_a?(String)
69
- # Try to load the class if it's defined as a string
70
- begin
71
- connection_class = config[:connection_class].constantize
72
- rescue NameError => e
73
- Rails.logger.warn "DBViewer could not load connection class #{config[:connection_class]}: #{e.message}"
74
- next
75
- end
76
- end
77
-
78
- if connection_class
79
- connection = connection_class.connection
80
- adapter_name = connection.adapter_name
81
- Rails.logger.info "DBViewer successfully connected to #{config[:name] || key.to_s} database (#{adapter_name})"
82
-
83
- # Store the resolved connection class back in the config
84
- config[:connection] = connection_class
85
- else
86
- raise "No valid connection configuration found for #{key}"
87
- end
88
- rescue => e
89
- connection_errors << { key: key, error: e.message }
90
- Rails.logger.error "DBViewer could not connect to #{config[:name] || key.to_s} database: #{e.message}"
91
- end
70
+ # Validate all configured database connections
71
+ def validate_database_connections
72
+ connection_errors = configuration.database_connections.filter_map do |key, config|
73
+ validate_single_connection(key, config)
92
74
  end
93
75
 
94
- if connection_errors.length == configuration.database_connections.length
95
- raise "DBViewer could not connect to any configured database"
96
- end
76
+ raise_if_all_connections_failed(connection_errors)
77
+ end
78
+
79
+ # Validate a single database connection
80
+ # @param key [Symbol] The connection key
81
+ # @param config [Hash] The connection configuration
82
+ # @return [Hash, nil] Error hash if validation failed, nil if successful
83
+ def validate_single_connection(key, config)
84
+ connection_class = resolve_connection_class(config)
85
+ return { key: key, error: "No valid connection configuration found for #{key}" } unless connection_class
86
+
87
+ test_connection(connection_class, config, key)
88
+ store_resolved_connection(config, connection_class)
89
+ nil
90
+ rescue => e
91
+ Rails.logger.error "DBViewer could not connect to #{config[:name] || key.to_s} database: #{e.message}"
92
+ { key: key, error: e.message }
97
93
  end
98
94
 
99
- # Initialize engine with default values or user-provided configuration
100
- def init
101
- # Define class methods to access configuration
102
- Dbviewer::SqlValidator.singleton_class.class_eval do
103
- define_method(:configuration) { Dbviewer.configuration }
104
- define_method(:max_query_length) { Dbviewer.configuration.max_query_length }
95
+ # Resolve the connection class from configuration
96
+ # @param config [Hash] The connection configuration
97
+ # @return [Class, nil] The resolved connection class or nil
98
+ def resolve_connection_class(config)
99
+ return config[:connection] if config[:connection]
100
+ return nil unless config[:connection_class].is_a?(String)
101
+
102
+ begin
103
+ config[:connection_class].constantize
104
+ rescue NameError => e
105
+ Rails.logger.warn "DBViewer could not load connection class #{config[:connection_class]}: #{e.message}"
106
+ nil
105
107
  end
108
+ end
109
+
110
+ # Test the database connection
111
+ # @param connection_class [Class] The connection class
112
+ # @param config [Hash] The connection configuration
113
+ # @param key [Symbol] The connection key
114
+ def test_connection(connection_class, config, key)
115
+ connection = connection_class.connection
116
+ adapter_name = connection.adapter_name
117
+ Rails.logger.info "DBViewer successfully connected to #{config[:name] || key.to_s} database (#{adapter_name})"
118
+ end
119
+
120
+ # Store the resolved connection class back in the config
121
+ # @param config [Hash] The connection configuration
122
+ # @param connection_class [Class] The resolved connection class
123
+ def store_resolved_connection(config, connection_class)
124
+ config[:connection] = connection_class
125
+ end
106
126
 
107
- # Log initialization
108
- Rails.logger.info("[DBViewer] Initialized with configuration: #{configuration.inspect}")
127
+ # Raise an error if all database connections failed
128
+ # @param connection_errors [Array] Array of connection error hashes
129
+ def raise_if_all_connections_failed(connection_errors)
130
+ if connection_errors.length == configuration.database_connections.length
131
+ raise "DBViewer could not connect to any configured database"
132
+ end
109
133
  end
110
134
  end
111
135
  end
@@ -18,4 +18,19 @@ Dbviewer.configure do |config|
18
18
  # username: "admin",
19
19
  # password: "your_secure_password"
20
20
  # }
21
+
22
+ # Multiple database connections configuration
23
+ # config.database_connections = {
24
+ # primary: {
25
+ # connection_class: "ActiveRecord::Base",
26
+ # name: "Primary Database"
27
+ # },
28
+ # secondary: {
29
+ # connection_class: "SecondaryDatabase",
30
+ # name: "Blog Database"
31
+ # }
32
+ # }
33
+
34
+ # Set the default active connection
35
+ # config.current_connection = :primary
21
36
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dbviewer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.2
4
+ version: 0.6.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wailan Tirajoh
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-06-04 00:00:00.000000000 Z
11
+ date: 2025-06-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -97,11 +97,13 @@ files:
97
97
  - lib/dbviewer/query/collection.rb
98
98
  - lib/dbviewer/query/executor.rb
99
99
  - lib/dbviewer/query/logger.rb
100
+ - lib/dbviewer/query/notification_subscriber.rb
100
101
  - lib/dbviewer/query/parser.rb
101
- - lib/dbviewer/sql_validator.rb
102
102
  - lib/dbviewer/storage/base.rb
103
103
  - lib/dbviewer/storage/file_storage.rb
104
104
  - lib/dbviewer/storage/in_memory_storage.rb
105
+ - lib/dbviewer/validator.rb
106
+ - lib/dbviewer/validator/sql.rb
105
107
  - lib/dbviewer/version.rb
106
108
  - lib/generators/dbviewer/install_generator.rb
107
109
  - lib/generators/dbviewer/templates/initializer.rb
@@ -1,194 +0,0 @@
1
- module Dbviewer
2
- # SqlValidator class handles SQL query validation and normalization
3
- # to ensure queries are safe (read-only) and properly formatted.
4
- # This helps prevent potentially destructive SQL operations.
5
- class SqlValidator
6
- # List of SQL keywords that could modify data or schema
7
- FORBIDDEN_KEYWORDS = %w[
8
- UPDATE INSERT DELETE DROP ALTER CREATE TRUNCATE REPLACE
9
- RENAME GRANT REVOKE LOCK UNLOCK COMMIT ROLLBACK
10
- SAVEPOINT INTO CALL EXECUTE EXEC
11
- ]
12
-
13
- # List of SQL keywords that should only be allowed in specific contexts
14
- CONDITIONAL_KEYWORDS = {
15
- # JOIN is allowed, but we should check for suspicious patterns
16
- "JOIN" => /\bJOIN\b/i,
17
- # UNION is allowed, but potential for injection
18
- "UNION" => /\bUNION\b/i,
19
- # WITH is allowed for CTEs, but need to ensure it's not a data modification
20
- "WITH" => /\bWITH\b/i
21
- }
22
-
23
- # Maximum allowed query length
24
- MAX_QUERY_LENGTH = 10000
25
-
26
- # Determines if a query is safe (read-only)
27
- # @param sql [String] The SQL query to validate
28
- # @return [Boolean] true if the query is safe, false otherwise
29
- def self.safe_query?(sql)
30
- return false if sql.blank?
31
-
32
- # Get max query length from configuration if available
33
- max_length = respond_to?(:max_query_length) ? max_query_length : MAX_QUERY_LENGTH
34
- return false if sql.length > max_length
35
-
36
- normalized_sql = normalize(sql)
37
-
38
- # Case-insensitive check for SELECT at the beginning
39
- return false unless normalized_sql =~ /\A\s*SELECT\s+/i
40
-
41
- # Check for forbidden keywords that might be used in subqueries or other SQL constructs
42
- FORBIDDEN_KEYWORDS.each do |keyword|
43
- # Look for the keyword with word boundaries to avoid false positives
44
- return false if normalized_sql =~ /\b#{keyword}\b/i
45
- end
46
-
47
- # Check for suspicious patterns that might indicate SQL injection attempts
48
- return false if has_suspicious_patterns?(normalized_sql)
49
-
50
- # Check for multiple statements (;) which could allow executing multiple commands
51
- statements = normalized_sql.split(";").reject(&:blank?)
52
- return false if statements.size > 1
53
-
54
- # Additional specific checks for common SQL injection patterns
55
- return false if has_injection_patterns?(normalized_sql)
56
-
57
- true
58
- end
59
-
60
- # Check for suspicious patterns in SQL that might indicate an attack
61
- # @param sql [String] Normalized SQL query
62
- # @return [Boolean] true if suspicious patterns found, false otherwise
63
- def self.has_suspicious_patterns?(sql)
64
- # Check for SQL comment sequences that might be used to hide malicious code
65
- return true if sql =~ /\s+--/ || sql =~ /\/\*/
66
-
67
- # Check for string concatenation which might be used for injection
68
- return true if sql =~ /\|\|/ || sql =~ /CONCAT\s*\(/i
69
-
70
- # Check for excessive number of quotes which might indicate injection
71
- single_quotes = sql.count("'")
72
- double_quotes = sql.count('"')
73
- return true if single_quotes > 20 || double_quotes > 20
74
-
75
- # Check for hex/binary data which might hide malicious code
76
- return true if sql =~ /0x[0-9a-f]{16,}/i
77
-
78
- false
79
- end
80
-
81
- # Check for specific SQL injection patterns
82
- # @param sql [String] Normalized SQL query
83
- # @return [Boolean] true if injection patterns found, false otherwise
84
- def self.has_injection_patterns?(sql)
85
- # Check for typical SQL injection test patterns
86
- return true if sql =~ /'\s*OR\s*'.*'\s*=\s*'/i
87
- return true if sql =~ /'\s*OR\s*1\s*=\s*1/i
88
- return true if sql =~ /'\s*;\s*--/i
89
-
90
- # Check for attempts to determine database type
91
- return true if sql =~ /@@version/i
92
- return true if sql =~ /version\(\)/i
93
-
94
- false
95
- end
96
-
97
- # Normalize SQL by removing comments and extra whitespace
98
- # @param sql [String] The SQL query to normalize
99
- # @return [String] The normalized SQL query
100
- def self.normalize(sql)
101
- return "" if sql.nil?
102
-
103
- begin
104
- # Remove SQL comments (both -- and /* */ styles)
105
- normalized = sql.gsub(/--.*$/, "") # Remove -- style comments
106
- .gsub(/\/\*.*?\*\//m, "") # Remove /* */ style comments
107
- .gsub(/\s+/, " ") # Normalize whitespace
108
- .strip # Remove leading/trailing whitespace
109
-
110
- # Replace multiple spaces with a single space
111
- normalized.gsub(/\s{2,}/, " ")
112
- rescue => e
113
- Rails.logger.error("[DBViewer] SQL normalization error: #{e.message}")
114
- ""
115
- end
116
- end
117
-
118
- # Validates a query and raises an exception if it's unsafe
119
- # @param sql [String] The SQL query to validate
120
- # @raise [SecurityError] if the query is unsafe
121
- # @return [String] The normalized SQL query if it's safe
122
- def self.validate!(sql)
123
- if sql.blank?
124
- raise SecurityError, "Empty query is not allowed"
125
- end
126
-
127
- # Get max query length from configuration if available
128
- max_length = respond_to?(:max_query_length) ? max_query_length : MAX_QUERY_LENGTH
129
- if sql.length > max_length
130
- raise SecurityError, "Query exceeds maximum allowed length (#{max_length} chars)"
131
- end
132
-
133
- normalized_sql = normalize(sql)
134
-
135
- # Special case for SQLite PRAGMA statements which are safe read-only commands
136
- if normalized_sql =~ /\A\s*PRAGMA\s+[a-z0-9_]+\s*\z/i
137
- return normalized_sql
138
- end
139
-
140
- unless normalized_sql =~ /\A\s*SELECT\s+/i
141
- raise SecurityError, "Query must begin with SELECT for security reasons"
142
- end
143
-
144
- FORBIDDEN_KEYWORDS.each do |keyword|
145
- if normalized_sql =~ /\b#{keyword}\b/i
146
- raise SecurityError, "Forbidden keyword '#{keyword}' detected in query"
147
- end
148
- end
149
-
150
- if has_suspicious_patterns?(normalized_sql)
151
- raise SecurityError, "Query contains suspicious patterns that may indicate SQL injection"
152
- end
153
-
154
- # Check for multiple statements
155
- statements = normalized_sql.split(";").reject(&:blank?)
156
- if statements.size > 1
157
- raise SecurityError, "Multiple SQL statements are not allowed"
158
- end
159
-
160
- if has_injection_patterns?(normalized_sql)
161
- raise SecurityError, "Query contains patterns commonly associated with SQL injection attempts"
162
- end
163
-
164
- normalized_sql
165
- end
166
-
167
- # Check if a query is using a specific database feature that might need special handling
168
- # @param sql [String] The SQL query
169
- # @param feature [Symbol] The feature to check for (:join, :subquery, :order_by, etc.)
170
- # @return [Boolean] true if the feature is used in the query
171
- def self.uses_feature?(sql, feature)
172
- normalized = normalize(sql)
173
- case feature
174
- when :join
175
- normalized =~ /\b(INNER|LEFT|RIGHT|FULL|CROSS)?\s*JOIN\b/i
176
- when :subquery
177
- # Check if there are parentheses that likely contain a subquery
178
- normalized.count("(") > normalized.count(")")
179
- when :order_by
180
- normalized =~ /\bORDER\s+BY\b/i
181
- when :group_by
182
- normalized =~ /\bGROUP\s+BY\b/i
183
- when :having
184
- normalized =~ /\bHAVING\b/i
185
- when :union
186
- normalized =~ /\bUNION\b/i
187
- when :window_function
188
- normalized =~ /\bOVER\s*\(/i
189
- else
190
- false
191
- end
192
- end
193
- end
194
- end