rails-mcp-server 1.0.1 → 1.1.0
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 +91 -169
- data/exe/rails-mcp-server +89 -17
- data/exe/rails-mcp-setup-claude +2 -1
- data/lib/rails-mcp-server/config.rb +72 -0
- data/lib/rails-mcp-server/tools/analyze_controller_views.rb +239 -0
- data/lib/rails-mcp-server/tools/analyze_environment_config.rb +427 -0
- data/lib/rails-mcp-server/tools/analyze_models.rb +116 -0
- data/lib/rails-mcp-server/tools/base_tool.rb +9 -0
- data/lib/rails-mcp-server/tools/get_file.rb +55 -0
- data/lib/rails-mcp-server/tools/get_model.rb +116 -0
- data/lib/rails-mcp-server/tools/get_routes.rb +24 -0
- data/lib/rails-mcp-server/tools/get_schema.rb +141 -0
- data/lib/rails-mcp-server/tools/list_files.rb +54 -0
- data/lib/rails-mcp-server/tools/project_info.rb +86 -0
- data/lib/rails-mcp-server/tools/switch_project.rb +25 -0
- data/lib/rails-mcp-server/utilities/run_process.rb +29 -0
- data/lib/rails-mcp-server/version.rb +1 -1
- data/lib/rails_mcp_server.rb +31 -864
- metadata +44 -3
@@ -0,0 +1,239 @@
|
|
1
|
+
module RailsMcpServer
|
2
|
+
class AnalyzeControllerViews < BaseTool
|
3
|
+
tool_name "analyze_controller_views"
|
4
|
+
|
5
|
+
description "Analyze the relationships between controllers, their actions, and corresponding views to understand the application's UI flow."
|
6
|
+
|
7
|
+
arguments do
|
8
|
+
optional(:controller_name).filled(:string).description("Name of a specific controller to analyze (e.g., 'UsersController' or 'users'). If omitted, all controllers will be analyzed.")
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(controller_name: nil)
|
12
|
+
unless current_project
|
13
|
+
message = "No active project. Please switch to a project first."
|
14
|
+
log(:warn, message)
|
15
|
+
|
16
|
+
return message
|
17
|
+
end
|
18
|
+
|
19
|
+
# Find all controllers
|
20
|
+
controllers_dir = File.join(active_project_path, "app", "controllers")
|
21
|
+
unless File.directory?(controllers_dir)
|
22
|
+
message = "Controllers directory not found at app/controllers."
|
23
|
+
log(:warn, message)
|
24
|
+
|
25
|
+
return message
|
26
|
+
end
|
27
|
+
|
28
|
+
# Get all controller files
|
29
|
+
controller_files = Dir.glob(File.join(controllers_dir, "**", "*_controller.rb"))
|
30
|
+
|
31
|
+
if controller_files.empty?
|
32
|
+
message = "No controllers found in the project."
|
33
|
+
log(:warn, message)
|
34
|
+
|
35
|
+
return message
|
36
|
+
end
|
37
|
+
|
38
|
+
# If a specific controller was requested, filter the files
|
39
|
+
if controller_name
|
40
|
+
# Normalize controller name (allow both 'users' and 'UsersController')
|
41
|
+
controller_name = "#{controller_name.sub(/_?controller$/i, "").downcase}_controller.rb"
|
42
|
+
controller_files = controller_files.select { |f| File.basename(f).downcase == controller_name }
|
43
|
+
|
44
|
+
if controller_files.empty?
|
45
|
+
message = "Controller '#{controller_name}' not found."
|
46
|
+
log(:warn, message)
|
47
|
+
|
48
|
+
return message
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Parse controllers to extract actions
|
53
|
+
controllers_data = {}
|
54
|
+
|
55
|
+
controller_files.each do |file_path|
|
56
|
+
file_content = File.read(file_path)
|
57
|
+
controller_class = File.basename(file_path, ".rb").gsub(/_controller$/i, "").then { |s| camelize(s) } + "Controller"
|
58
|
+
|
59
|
+
# Extract controller actions (methods that are not private/protected)
|
60
|
+
actions = []
|
61
|
+
action_matches = file_content.scan(/def\s+([a-zA-Z0-9_]+)/).flatten
|
62
|
+
|
63
|
+
# Find where private/protected begins
|
64
|
+
private_index = file_content =~ /^\s*(private|protected)/
|
65
|
+
|
66
|
+
if private_index
|
67
|
+
# Get the actions defined before private/protected
|
68
|
+
private_content = file_content[private_index..-1]
|
69
|
+
private_methods = private_content.scan(/def\s+([a-zA-Z0-9_]+)/).flatten
|
70
|
+
actions = action_matches - private_methods
|
71
|
+
else
|
72
|
+
actions = action_matches
|
73
|
+
end
|
74
|
+
|
75
|
+
# Remove Rails controller lifecycle methods
|
76
|
+
lifecycle_methods = %w[initialize action_name controller_name params response]
|
77
|
+
actions -= lifecycle_methods
|
78
|
+
|
79
|
+
# Get routes mapped to this controller
|
80
|
+
routes_output = RailsMcpServer::RunProcess.execute_rails_command(
|
81
|
+
active_project_path,
|
82
|
+
"bin/rails routes -c #{controller_class}"
|
83
|
+
)
|
84
|
+
|
85
|
+
routes = {}
|
86
|
+
if routes_output && !routes_output.empty?
|
87
|
+
routes_output.split("\n").each do |line|
|
88
|
+
next if line.include?("(erb):") || line.include?("Prefix") || line.strip.empty?
|
89
|
+
parts = line.strip.split(/\s+/)
|
90
|
+
if parts.size >= 4
|
91
|
+
# Get action name from the rails routes output
|
92
|
+
action = parts[1].to_s.strip.downcase
|
93
|
+
if actions.include?(action)
|
94
|
+
verb = parts[0].to_s.strip
|
95
|
+
path = parts[2].to_s.strip
|
96
|
+
routes[action] = {verb: verb, path: path}
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# Find views for each action
|
103
|
+
views_dir = File.join(active_project_path, "app", "views", File.basename(file_path, "_controller.rb"))
|
104
|
+
views = {}
|
105
|
+
|
106
|
+
if File.directory?(views_dir)
|
107
|
+
actions.each do |action|
|
108
|
+
# Look for view templates with various extensions
|
109
|
+
view_files = Dir.glob(File.join(views_dir, "#{action}.*"))
|
110
|
+
if view_files.any?
|
111
|
+
views[action] = {
|
112
|
+
templates: view_files.map { |f| f.sub("#{active_project_path}/", "") },
|
113
|
+
partials: []
|
114
|
+
}
|
115
|
+
|
116
|
+
# Look for partials used in this template
|
117
|
+
view_files.each do |view_file|
|
118
|
+
if File.file?(view_file)
|
119
|
+
view_content = File.read(view_file)
|
120
|
+
# Find render calls with partials
|
121
|
+
partial_matches = view_content.scan(/render\s+(?:partial:|:partial\s+=>\s+|:partial\s*=>|partial:)\s*["']([^"']+)["']/).flatten
|
122
|
+
views[action][:partials] += partial_matches if partial_matches.any?
|
123
|
+
|
124
|
+
# Find instance variables used in the view
|
125
|
+
instance_vars = view_content.scan(/@([a-zA-Z0-9_]+)/).flatten.uniq # rubocop:disable Performance/ChainArrayAllocation
|
126
|
+
views[action][:instance_variables] = instance_vars if instance_vars.any?
|
127
|
+
|
128
|
+
# Look for Stimulus controllers
|
129
|
+
stimulus_controllers = view_content.scan(/data-controller="([^"]+)"/).flatten.uniq # rubocop:disable Performance/ChainArrayAllocation
|
130
|
+
views[action][:stimulus_controllers] = stimulus_controllers if stimulus_controllers.any?
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# Extract instance variables set in the controller action
|
138
|
+
instance_vars_in_controller = {}
|
139
|
+
actions.each do |action|
|
140
|
+
# Find the action method in the controller
|
141
|
+
action_match = file_content.match(/def\s+#{action}\b(.*?)(?:(?:def|private|protected|public)\b|\z)/m)
|
142
|
+
if action_match && action_match[1]
|
143
|
+
action_body = action_match[1]
|
144
|
+
# Find instance variable assignments
|
145
|
+
vars = action_body.scan(/@([a-zA-Z0-9_]+)\s*=/).flatten.uniq # rubocop:disable Performance/ChainArrayAllocation
|
146
|
+
instance_vars_in_controller[action] = vars if vars.any?
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
controllers_data[controller_class] = {
|
151
|
+
file: file_path.sub("#{active_project_path}/", ""),
|
152
|
+
actions: actions,
|
153
|
+
routes: routes,
|
154
|
+
views: views,
|
155
|
+
instance_variables: instance_vars_in_controller
|
156
|
+
}
|
157
|
+
rescue => e
|
158
|
+
log(:error, "Error parsing controller #{file_path}: #{e.message}")
|
159
|
+
end
|
160
|
+
|
161
|
+
# Format the output
|
162
|
+
output = []
|
163
|
+
|
164
|
+
controllers_data.each do |controller, data|
|
165
|
+
output << "Controller: #{controller}"
|
166
|
+
output << " File: #{data[:file]}"
|
167
|
+
output << " Actions: #{data[:actions].size}"
|
168
|
+
|
169
|
+
data[:actions].each do |action|
|
170
|
+
output << " Action: #{action}"
|
171
|
+
|
172
|
+
# Show route if available
|
173
|
+
if data[:routes] && data[:routes][action]
|
174
|
+
route = data[:routes][action]
|
175
|
+
output << " Route: [#{route[:verb]}] #{route[:path]}"
|
176
|
+
else
|
177
|
+
output << " Route: Not mapped to a route"
|
178
|
+
end
|
179
|
+
|
180
|
+
# Show view templates if available
|
181
|
+
if data[:views] && data[:views][action]
|
182
|
+
view_data = data[:views][action]
|
183
|
+
|
184
|
+
output << " View Templates:"
|
185
|
+
view_data[:templates].each do |template|
|
186
|
+
output << " - #{template}"
|
187
|
+
end
|
188
|
+
|
189
|
+
# Show partials
|
190
|
+
if view_data[:partials]&.any?
|
191
|
+
output << " Partials Used:"
|
192
|
+
view_data[:partials].uniq.each do |partial|
|
193
|
+
output << " - #{partial}"
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
# Show Stimulus controllers
|
198
|
+
if view_data[:stimulus_controllers]&.any?
|
199
|
+
output << " Stimulus Controllers:"
|
200
|
+
view_data[:stimulus_controllers].each do |controller|
|
201
|
+
output << " - #{controller}"
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
# Show instance variables used in views
|
206
|
+
if view_data[:instance_variables]&.any?
|
207
|
+
output << " Instance Variables Used in View:"
|
208
|
+
view_data[:instance_variables].sort.each do |var|
|
209
|
+
output << " - @#{var}"
|
210
|
+
end
|
211
|
+
end
|
212
|
+
else
|
213
|
+
output << " View: No view template found"
|
214
|
+
end
|
215
|
+
|
216
|
+
# Show instance variables set in controller
|
217
|
+
if data[:instance_variables] && data[:instance_variables][action]
|
218
|
+
output << " Instance Variables Set in Controller:"
|
219
|
+
data[:instance_variables][action].sort.each do |var|
|
220
|
+
output << " - @#{var}"
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
output << ""
|
225
|
+
end
|
226
|
+
|
227
|
+
output << "-------------------------"
|
228
|
+
end
|
229
|
+
|
230
|
+
output.join("\n")
|
231
|
+
end
|
232
|
+
|
233
|
+
private
|
234
|
+
|
235
|
+
def camelize(string)
|
236
|
+
string.split("_").map(&:capitalize).join
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
@@ -0,0 +1,427 @@
|
|
1
|
+
module RailsMcpServer
|
2
|
+
class AnalyzeEnvironmentConfig < BaseTool
|
3
|
+
tool_name "analyze_environment_config"
|
4
|
+
|
5
|
+
description "Analyze environment configurations to identify inconsistencies, security issues, and missing variables across environments."
|
6
|
+
|
7
|
+
def call
|
8
|
+
unless current_project
|
9
|
+
message = "No active project. Please switch to a project first."
|
10
|
+
log(:warn, message)
|
11
|
+
|
12
|
+
return message
|
13
|
+
end
|
14
|
+
|
15
|
+
# Check for required directories and files
|
16
|
+
env_dir = File.join(active_project_path, "config", "environments")
|
17
|
+
unless File.directory?(env_dir)
|
18
|
+
message = "Environment configuration directory not found at config/environments."
|
19
|
+
log(:warn, message)
|
20
|
+
|
21
|
+
return message
|
22
|
+
end
|
23
|
+
|
24
|
+
# Initialize data structures
|
25
|
+
env_files = {}
|
26
|
+
env_settings = {}
|
27
|
+
|
28
|
+
# 1. Parse environment files
|
29
|
+
Dir.glob(File.join(env_dir, "*.rb")).each do |file|
|
30
|
+
env_name = File.basename(file, ".rb")
|
31
|
+
env_files[env_name] = file
|
32
|
+
env_content = File.read(file)
|
33
|
+
|
34
|
+
# Extract settings from environment files
|
35
|
+
env_settings[env_name] = extract_env_settings(env_content)
|
36
|
+
end
|
37
|
+
|
38
|
+
# 2. Find ENV variable usage across the codebase
|
39
|
+
env_vars_in_code = find_env_vars_in_codebase(active_project_path)
|
40
|
+
|
41
|
+
# 3. Check for .env files and their variables
|
42
|
+
dotenv_files = {}
|
43
|
+
dotenv_vars = {}
|
44
|
+
|
45
|
+
# Common .env file patterns
|
46
|
+
dotenv_patterns = [
|
47
|
+
".env",
|
48
|
+
".env.development",
|
49
|
+
".env.test",
|
50
|
+
".env.production",
|
51
|
+
".env.local",
|
52
|
+
".env.development.local",
|
53
|
+
".env.test.local",
|
54
|
+
".env.production.local"
|
55
|
+
]
|
56
|
+
|
57
|
+
dotenv_patterns.each do |pattern|
|
58
|
+
file_path = File.join(active_project_path, pattern)
|
59
|
+
if File.exist?(file_path)
|
60
|
+
dotenv_files[pattern] = file_path
|
61
|
+
dotenv_vars[pattern] = parse_dotenv_file(file_path)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# 4. Check credentials files
|
66
|
+
credentials_files = {}
|
67
|
+
credentials_key_file = File.join(active_project_path, "config", "master.key")
|
68
|
+
credentials_file = File.join(active_project_path, "config", "credentials.yml.enc")
|
69
|
+
|
70
|
+
if File.exist?(credentials_file)
|
71
|
+
credentials_files["credentials.yml.enc"] = credentials_file
|
72
|
+
end
|
73
|
+
|
74
|
+
# Environment-specific credentials files
|
75
|
+
Dir.glob(File.join(active_project_path, "config", "credentials", "*.yml.enc")).each do |file|
|
76
|
+
env_name = File.basename(file, ".yml.enc")
|
77
|
+
credentials_files["credentials/#{env_name}.yml.enc"] = file
|
78
|
+
end
|
79
|
+
|
80
|
+
# 5. Check database configuration
|
81
|
+
database_config_file = File.join(active_project_path, "config", "database.yml")
|
82
|
+
database_config = {}
|
83
|
+
|
84
|
+
if File.exist?(database_config_file)
|
85
|
+
database_config = parse_database_config(database_config_file)
|
86
|
+
end
|
87
|
+
|
88
|
+
# 6. Generate findings
|
89
|
+
|
90
|
+
# 6.1. Compare environment settings
|
91
|
+
env_diff = compare_environment_settings(env_settings)
|
92
|
+
|
93
|
+
# 6.2. Find missing ENV variables
|
94
|
+
missing_env_vars = find_missing_env_vars(env_vars_in_code, dotenv_vars)
|
95
|
+
|
96
|
+
# 6.3. Check for potential security issues
|
97
|
+
security_findings = check_security_configuration(env_settings, database_config)
|
98
|
+
|
99
|
+
# Format the output
|
100
|
+
output = []
|
101
|
+
|
102
|
+
# Environment files summary
|
103
|
+
output << "Environment Configuration Analysis"
|
104
|
+
output << "=================================="
|
105
|
+
output << ""
|
106
|
+
output << "Environment Files:"
|
107
|
+
env_files.each do |env, file|
|
108
|
+
output << " - #{env}: #{file.sub("#{active_project_path}/", "")}"
|
109
|
+
end
|
110
|
+
output << ""
|
111
|
+
|
112
|
+
# Environment variables summary
|
113
|
+
output << "Environment Variables Usage:"
|
114
|
+
output << " Total unique ENV variables found in codebase: #{env_vars_in_code.keys.size}"
|
115
|
+
output << ""
|
116
|
+
|
117
|
+
# Missing ENV variables
|
118
|
+
if missing_env_vars.any?
|
119
|
+
output << "Missing ENV Variables:"
|
120
|
+
missing_env_vars.each do |env_var, environments|
|
121
|
+
output << " - #{env_var}: Used in codebase but missing in #{environments.join(", ")}"
|
122
|
+
end
|
123
|
+
else
|
124
|
+
output << "All ENV variables appear to be defined in at least one .env file."
|
125
|
+
end
|
126
|
+
output << ""
|
127
|
+
|
128
|
+
# Environment differences
|
129
|
+
if env_diff[:unique_settings].any?
|
130
|
+
output << "Environment-Specific Settings:"
|
131
|
+
env_diff[:unique_settings].each do |env, settings|
|
132
|
+
output << " #{env}:"
|
133
|
+
settings.each do |setting|
|
134
|
+
output << " - #{setting}"
|
135
|
+
end
|
136
|
+
end
|
137
|
+
output << ""
|
138
|
+
end
|
139
|
+
|
140
|
+
if env_diff[:different_values].any?
|
141
|
+
output << "Settings with Different Values Across Environments:"
|
142
|
+
env_diff[:different_values].each do |setting, values|
|
143
|
+
output << " #{setting}:"
|
144
|
+
values.each do |env, value|
|
145
|
+
output << " - #{env}: #{value}"
|
146
|
+
end
|
147
|
+
end
|
148
|
+
output << ""
|
149
|
+
end
|
150
|
+
|
151
|
+
# Credentials files
|
152
|
+
output << "Credentials Management:"
|
153
|
+
if credentials_files.any?
|
154
|
+
output << " Encrypted credentials files found:"
|
155
|
+
credentials_files.each do |name, file|
|
156
|
+
output << " - #{name}"
|
157
|
+
end
|
158
|
+
|
159
|
+
output << if File.exist?(credentials_key_file)
|
160
|
+
" Master key file exists (config/master.key)"
|
161
|
+
else
|
162
|
+
" Warning: No master.key file found. Credentials are likely managed through RAILS_MASTER_KEY environment variable."
|
163
|
+
end
|
164
|
+
else
|
165
|
+
output << " No encrypted credentials files found. The application may be using ENV variables exclusively."
|
166
|
+
end
|
167
|
+
output << ""
|
168
|
+
|
169
|
+
# Database configuration
|
170
|
+
output << "Database Configuration:"
|
171
|
+
if database_config.any?
|
172
|
+
database_config.each do |env, config|
|
173
|
+
output << " #{env}:"
|
174
|
+
# Show connection details without exposing passwords
|
175
|
+
if config["adapter"]
|
176
|
+
output << " - Adapter: #{config["adapter"]}"
|
177
|
+
end
|
178
|
+
if config["host"] && config["host"] != "localhost" && config["host"] != "127.0.0.1"
|
179
|
+
output << " - Host: #{config["host"]}"
|
180
|
+
end
|
181
|
+
if config["database"]
|
182
|
+
output << " - Database: #{config["database"]}"
|
183
|
+
end
|
184
|
+
|
185
|
+
# Check for credentials in database.yml
|
186
|
+
if config["username"] && !config["username"].include?("ENV")
|
187
|
+
output << " - Warning: Database username hardcoded in database.yml"
|
188
|
+
end
|
189
|
+
if config["password"] && !config["password"].include?("ENV")
|
190
|
+
output << " - Warning: Database password hardcoded in database.yml"
|
191
|
+
end
|
192
|
+
end
|
193
|
+
else
|
194
|
+
output << " Could not parse database configuration."
|
195
|
+
end
|
196
|
+
output << ""
|
197
|
+
|
198
|
+
# Security findings
|
199
|
+
if security_findings.any?
|
200
|
+
output << "Security Configuration Findings:"
|
201
|
+
security_findings.each do |finding|
|
202
|
+
output << " - #{finding}"
|
203
|
+
end
|
204
|
+
output << ""
|
205
|
+
end
|
206
|
+
|
207
|
+
output.join("\n")
|
208
|
+
end
|
209
|
+
|
210
|
+
private
|
211
|
+
|
212
|
+
# Helper method to extract settings from environment files
|
213
|
+
def extract_env_settings(content)
|
214
|
+
settings = {}
|
215
|
+
|
216
|
+
# Match configuration settings
|
217
|
+
content.scan(/config\.([a-zA-Z0-9_.]+)\s*=\s*([^#\n]+)/) do |match|
|
218
|
+
key = match[0].strip
|
219
|
+
value = match[1].strip
|
220
|
+
|
221
|
+
# Clean up the value
|
222
|
+
value = value.chomp(";").strip
|
223
|
+
|
224
|
+
settings[key] = value
|
225
|
+
end
|
226
|
+
|
227
|
+
settings
|
228
|
+
end
|
229
|
+
|
230
|
+
# Helper method to find ENV variable usage in the codebase
|
231
|
+
def find_env_vars_in_codebase(project_path)
|
232
|
+
env_vars = {}
|
233
|
+
|
234
|
+
# Define directories to search
|
235
|
+
search_dirs = [
|
236
|
+
File.join(project_path, "app"),
|
237
|
+
File.join(project_path, "config"),
|
238
|
+
File.join(project_path, "lib")
|
239
|
+
]
|
240
|
+
|
241
|
+
# Define file patterns to search
|
242
|
+
file_patterns = ["*.rb", "*.yml", "*.erb", "*.js"]
|
243
|
+
|
244
|
+
search_dirs.each do |dir|
|
245
|
+
if File.directory?(dir)
|
246
|
+
file_patterns.each do |pattern|
|
247
|
+
Dir.glob(File.join(dir, "**", pattern)).each do |file|
|
248
|
+
content = File.read(file)
|
249
|
+
|
250
|
+
# Extract ENV variables
|
251
|
+
content.scan(/ENV\s*\[\s*['"]([^'"]+)['"]\s*\]/).each do |match|
|
252
|
+
env_var = match[0]
|
253
|
+
env_vars[env_var] ||= []
|
254
|
+
env_vars[env_var] << file.sub("#{project_path}/", "")
|
255
|
+
end
|
256
|
+
|
257
|
+
# Also match ENV['VAR'] pattern
|
258
|
+
content.scan(/ENV\s*\.\s*\[\s*['"]([^'"]+)['"]\s*\]/).each do |match|
|
259
|
+
env_var = match[0]
|
260
|
+
env_vars[env_var] ||= []
|
261
|
+
env_vars[env_var] << file.sub("#{project_path}/", "")
|
262
|
+
end
|
263
|
+
|
264
|
+
# Also match ENV.fetch('VAR') pattern
|
265
|
+
content.scan(/ENV\s*\.\s*fetch\s*\(\s*['"]([^'"]+)['"]\s*/).each do |match|
|
266
|
+
env_var = match[0]
|
267
|
+
env_vars[env_var] ||= []
|
268
|
+
env_vars[env_var] << file.sub("#{project_path}/", "")
|
269
|
+
end
|
270
|
+
rescue => e
|
271
|
+
log(:error, "Error reading file #{file}: #{e.message}")
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
env_vars
|
278
|
+
end
|
279
|
+
|
280
|
+
# Helper method to parse .env files
|
281
|
+
def parse_dotenv_file(file_path)
|
282
|
+
vars = {}
|
283
|
+
|
284
|
+
begin
|
285
|
+
File.readlines(file_path).each do |line| # rubocop:disable Performance/IoReadlines
|
286
|
+
# Skip comments and empty lines
|
287
|
+
next if line.strip.empty? || line.strip.start_with?("#")
|
288
|
+
|
289
|
+
# Parse KEY=value pattern
|
290
|
+
if line =~ /\A([A-Za-z0-9_]+)=(.*)\z/
|
291
|
+
key = $1
|
292
|
+
# Store just the existence of the variable, not its value
|
293
|
+
vars[key] = true
|
294
|
+
end
|
295
|
+
end
|
296
|
+
rescue => e
|
297
|
+
log(:error, "Error parsing .env file #{file_path}: #{e.message}")
|
298
|
+
end
|
299
|
+
|
300
|
+
vars
|
301
|
+
end
|
302
|
+
|
303
|
+
# Helper method to parse database.yml
|
304
|
+
def parse_database_config(file_path)
|
305
|
+
config = {}
|
306
|
+
|
307
|
+
begin
|
308
|
+
# Simple YAML parsing - not handling ERB
|
309
|
+
yaml_content = File.read(file_path)
|
310
|
+
yaml_data = YAML.safe_load(yaml_content) || {}
|
311
|
+
|
312
|
+
# Extract environment configurations
|
313
|
+
%w[development test production staging].each do |env|
|
314
|
+
config[env] = yaml_data[env] if yaml_data[env]
|
315
|
+
end
|
316
|
+
rescue => e
|
317
|
+
log(:error, "Error parsing database.yml: #{e.message}")
|
318
|
+
end
|
319
|
+
|
320
|
+
config
|
321
|
+
end
|
322
|
+
|
323
|
+
# Helper method to compare environment settings
|
324
|
+
def compare_environment_settings(env_settings)
|
325
|
+
result = {
|
326
|
+
unique_settings: {},
|
327
|
+
different_values: {}
|
328
|
+
}
|
329
|
+
|
330
|
+
# Get all settings across all environments
|
331
|
+
all_settings = env_settings.values.map(&:keys).flatten.uniq # rubocop:disable Performance/ChainArrayAllocation
|
332
|
+
|
333
|
+
# Find settings unique to certain environments
|
334
|
+
env_settings.each do |env, settings|
|
335
|
+
unique = settings.keys - (all_settings - settings.keys)
|
336
|
+
result[:unique_settings][env] = unique if unique.any?
|
337
|
+
end
|
338
|
+
|
339
|
+
# Find settings with different values across environments
|
340
|
+
all_settings.each do |setting|
|
341
|
+
values = {}
|
342
|
+
|
343
|
+
env_settings.each do |env, settings|
|
344
|
+
values[env] = settings[setting] if settings[setting]
|
345
|
+
end
|
346
|
+
|
347
|
+
# Only include if there are different values
|
348
|
+
if values.values.uniq.size > 1
|
349
|
+
result[:different_values][setting] = values
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
result
|
354
|
+
end
|
355
|
+
|
356
|
+
# Helper method to find missing ENV variables
|
357
|
+
def find_missing_env_vars(env_vars_in_code, dotenv_vars)
|
358
|
+
missing_vars = {}
|
359
|
+
|
360
|
+
# Check each ENV variable used in code
|
361
|
+
env_vars_in_code.each do |var, files|
|
362
|
+
# Environments where the variable is missing
|
363
|
+
missing_in = []
|
364
|
+
|
365
|
+
# Check in each .env file
|
366
|
+
if dotenv_vars.empty?
|
367
|
+
missing_in << "all environments (no .env files found)"
|
368
|
+
else
|
369
|
+
dotenv_vars.each do |env_file, vars|
|
370
|
+
env_name = env_file.gsub(/^\.env\.?|\.local$/, "")
|
371
|
+
env_name = "development" if env_name.empty?
|
372
|
+
|
373
|
+
if !vars.key?(var)
|
374
|
+
missing_in << env_name
|
375
|
+
end
|
376
|
+
end
|
377
|
+
end
|
378
|
+
|
379
|
+
missing_vars[var] = missing_in if missing_in.any?
|
380
|
+
end
|
381
|
+
|
382
|
+
missing_vars
|
383
|
+
end
|
384
|
+
|
385
|
+
# Helper method to check for security issues
|
386
|
+
def check_security_configuration(env_settings, database_config)
|
387
|
+
findings = []
|
388
|
+
|
389
|
+
# Check for common security settings
|
390
|
+
env_settings.each do |env, settings|
|
391
|
+
# Check for secure cookies in production
|
392
|
+
if env == "production"
|
393
|
+
if settings["cookies.secure"] == "false"
|
394
|
+
findings << "Production has cookies.secure = false"
|
395
|
+
end
|
396
|
+
|
397
|
+
if settings["session_store.secure"] == "false"
|
398
|
+
findings << "Production has session_store.secure = false"
|
399
|
+
end
|
400
|
+
|
401
|
+
# Force SSL
|
402
|
+
if settings["force_ssl"] == "false"
|
403
|
+
findings << "Production has force_ssl = false"
|
404
|
+
end
|
405
|
+
end
|
406
|
+
|
407
|
+
# Check for CSRF protection
|
408
|
+
if settings["action_controller.default_protect_from_forgery"] == "false"
|
409
|
+
findings << "#{env} has CSRF protection disabled"
|
410
|
+
end
|
411
|
+
end
|
412
|
+
|
413
|
+
# Check for hardcoded credentials in database.yml
|
414
|
+
database_config.each do |env, config|
|
415
|
+
if config["username"] && !config["username"].include?("ENV")
|
416
|
+
findings << "Database username hardcoded in database.yml for #{env}"
|
417
|
+
end
|
418
|
+
|
419
|
+
if config["password"] && !config["password"].include?("ENV")
|
420
|
+
findings << "Database password hardcoded in database.yml for #{env}"
|
421
|
+
end
|
422
|
+
end
|
423
|
+
|
424
|
+
findings
|
425
|
+
end
|
426
|
+
end
|
427
|
+
end
|