makit 0.0.140 → 0.0.141
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 +41 -41
- data/exe/makit +5 -5
- data/lib/makit/apache.rb +28 -28
- data/lib/makit/auto.rb +48 -48
- data/lib/makit/cli/build_commands.rb +500 -500
- data/lib/makit/cli/generators/base_generator.rb +74 -74
- data/lib/makit/cli/generators/dotnet_generator.rb +50 -50
- data/lib/makit/cli/generators/generator_factory.rb +49 -49
- data/lib/makit/cli/generators/node_generator.rb +50 -50
- data/lib/makit/cli/generators/ruby_generator.rb +77 -77
- data/lib/makit/cli/generators/rust_generator.rb +50 -50
- data/lib/makit/cli/generators/templates/dotnet_templates.rb +167 -167
- data/lib/makit/cli/generators/templates/node_templates.rb +161 -161
- data/lib/makit/cli/generators/templates/ruby/gemfile.rb +26 -26
- data/lib/makit/cli/generators/templates/ruby/gemspec.rb +40 -40
- data/lib/makit/cli/generators/templates/ruby/main_lib.rb +33 -33
- data/lib/makit/cli/generators/templates/ruby/rakefile.rb +35 -35
- data/lib/makit/cli/generators/templates/ruby/readme.rb +63 -63
- data/lib/makit/cli/generators/templates/ruby/test.rb +39 -39
- data/lib/makit/cli/generators/templates/ruby/test_helper.rb +29 -29
- data/lib/makit/cli/generators/templates/ruby/version.rb +29 -29
- data/lib/makit/cli/generators/templates/rust_templates.rb +128 -128
- data/lib/makit/cli/main.rb +69 -69
- data/lib/makit/cli/project_commands.rb +868 -868
- data/lib/makit/cli/repository_commands.rb +661 -661
- data/lib/makit/cli/strategy_commands.rb +203 -203
- data/lib/makit/cli/utility_commands.rb +521 -521
- data/lib/makit/commands/factory.rb +359 -359
- data/lib/makit/commands/middleware/base.rb +73 -73
- data/lib/makit/commands/middleware/cache.rb +248 -248
- data/lib/makit/commands/middleware/command_logger.rb +312 -312
- data/lib/makit/commands/middleware/validator.rb +269 -269
- data/lib/makit/commands/request.rb +316 -316
- data/lib/makit/commands/result.rb +323 -323
- data/lib/makit/commands/runner.rb +388 -385
- data/lib/makit/commands/strategies/base.rb +171 -171
- data/lib/makit/commands/strategies/child_process.rb +165 -165
- data/lib/makit/commands/strategies/factory.rb +136 -136
- data/lib/makit/commands/strategies/synchronous.rb +139 -139
- data/lib/makit/commands.rb +50 -50
- data/lib/makit/configuration/dotnet_project.rb +12 -12
- data/lib/makit/configuration/gitlab_helper.rb +58 -58
- data/lib/makit/configuration/project.rb +168 -168
- data/lib/makit/configuration/rakefile_helper.rb +43 -43
- data/lib/makit/configuration/step.rb +34 -34
- data/lib/makit/configuration/timeout.rb +74 -74
- data/lib/makit/configuration.rb +15 -15
- data/lib/makit/content/default_gitignore.rb +7 -7
- data/lib/makit/content/default_gitignore.txt +225 -225
- data/lib/makit/content/default_rakefile.rb +13 -13
- data/lib/makit/content/gem_rakefile.rb +16 -16
- data/lib/makit/context.rb +1 -1
- data/lib/makit/data.rb +49 -49
- data/lib/makit/directories.rb +140 -140
- data/lib/makit/directory.rb +262 -262
- data/lib/makit/docs/files.rb +89 -89
- data/lib/makit/docs/rake.rb +102 -102
- data/lib/makit/dotnet/cli.rb +69 -69
- data/lib/makit/dotnet/project.rb +217 -217
- data/lib/makit/dotnet/solution.rb +38 -38
- data/lib/makit/dotnet/solution_classlib.rb +239 -239
- data/lib/makit/dotnet/solution_console.rb +264 -264
- data/lib/makit/dotnet/solution_maui.rb +354 -354
- data/lib/makit/dotnet/solution_wasm.rb +275 -275
- data/lib/makit/dotnet/solution_wpf.rb +304 -304
- data/lib/makit/dotnet.rb +102 -102
- data/lib/makit/email.rb +90 -90
- data/lib/makit/environment.rb +142 -142
- data/lib/makit/examples/runner.rb +370 -370
- data/lib/makit/exceptions.rb +45 -45
- data/lib/makit/fileinfo.rb +24 -24
- data/lib/makit/files.rb +43 -43
- data/lib/makit/gems.rb +40 -40
- data/lib/makit/git/cli.rb +54 -54
- data/lib/makit/git/repository.rb +90 -90
- data/lib/makit/git.rb +98 -98
- data/lib/makit/gitlab_runner.rb +59 -59
- data/lib/makit/humanize.rb +137 -137
- data/lib/makit/indexer.rb +47 -47
- data/lib/makit/logging/configuration.rb +308 -308
- data/lib/makit/logging/format_registry.rb +84 -84
- data/lib/makit/logging/formatters/base.rb +39 -39
- data/lib/makit/logging/formatters/console_formatter.rb +140 -140
- data/lib/makit/logging/formatters/json_formatter.rb +65 -65
- data/lib/makit/logging/formatters/plain_text_formatter.rb +71 -71
- data/lib/makit/logging/formatters/text_formatter.rb +64 -64
- data/lib/makit/logging/log_request.rb +119 -119
- data/lib/makit/logging/logger.rb +199 -199
- data/lib/makit/logging/sinks/base.rb +91 -91
- data/lib/makit/logging/sinks/console.rb +72 -72
- data/lib/makit/logging/sinks/file_sink.rb +92 -92
- data/lib/makit/logging/sinks/structured.rb +123 -123
- data/lib/makit/logging/sinks/unified_file_sink.rb +296 -296
- data/lib/makit/logging.rb +565 -565
- data/lib/makit/markdown.rb +75 -75
- data/lib/makit/mp/basic_object_mp.rb +17 -17
- data/lib/makit/mp/command_mp.rb +13 -13
- data/lib/makit/mp/command_request.mp.rb +17 -17
- data/lib/makit/mp/project_mp.rb +199 -199
- data/lib/makit/mp/string_mp.rb +199 -191
- data/lib/makit/nuget.rb +74 -74
- data/lib/makit/port.rb +32 -32
- data/lib/makit/process.rb +163 -163
- data/lib/makit/protoc.rb +107 -107
- data/lib/makit/rake/cli.rb +196 -196
- data/lib/makit/rake/trace_controller.rb +173 -173
- data/lib/makit/rake.rb +80 -80
- data/lib/makit/ruby/cli.rb +185 -185
- data/lib/makit/ruby.rb +25 -25
- data/lib/makit/secrets.rb +51 -51
- data/lib/makit/serializer.rb +130 -130
- data/lib/makit/services/builder.rb +186 -186
- data/lib/makit/services/error_handler.rb +226 -226
- data/lib/makit/services/repository_manager.rb +231 -231
- data/lib/makit/services/validator.rb +112 -112
- data/lib/makit/setup/classlib.rb +101 -101
- data/lib/makit/setup/gem.rb +268 -268
- data/lib/makit/setup/razorclasslib.rb +101 -101
- data/lib/makit/setup/runner.rb +54 -54
- data/lib/makit/setup.rb +5 -5
- data/lib/makit/show.rb +110 -110
- data/lib/makit/storage.rb +126 -126
- data/lib/makit/symbols.rb +170 -170
- data/lib/makit/task_info.rb +130 -130
- data/lib/makit/tasks/at_exit.rb +15 -15
- data/lib/makit/tasks/build.rb +22 -22
- data/lib/makit/tasks/clean.rb +13 -13
- data/lib/makit/tasks/configure.rb +10 -10
- data/lib/makit/tasks/format.rb +10 -10
- data/lib/makit/tasks/hook_manager.rb +443 -443
- data/lib/makit/tasks/init.rb +49 -49
- data/lib/makit/tasks/integrate.rb +29 -29
- data/lib/makit/tasks/pull_incoming.rb +13 -13
- data/lib/makit/tasks/setup.rb +13 -13
- data/lib/makit/tasks/sync.rb +17 -17
- data/lib/makit/tasks/tag.rb +16 -16
- data/lib/makit/tasks/task_monkey_patch.rb +81 -81
- data/lib/makit/tasks/test.rb +22 -22
- data/lib/makit/tasks/update.rb +18 -18
- data/lib/makit/tasks.rb +20 -20
- data/lib/makit/test_cache.rb +239 -239
- data/lib/makit/tree.rb +37 -37
- data/lib/makit/v1/makit.v1_pb.rb +35 -35
- data/lib/makit/v1/makit.v1_services_pb.rb +27 -27
- data/lib/makit/version.rb +99 -99
- data/lib/makit/version_util.rb +21 -21
- data/lib/makit/wix.rb +95 -95
- data/lib/makit/yaml.rb +29 -29
- data/lib/makit/zip.rb +17 -17
- data/lib/makit copy.rb +44 -44
- data/lib/makit.rb +42 -42
- metadata +2 -2
@@ -1,269 +1,269 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "shellwords"
|
4
|
-
require "uri"
|
5
|
-
|
6
|
-
module Makit
|
7
|
-
module Commands
|
8
|
-
# Custom exception for security-related command validation failures
|
9
|
-
class SecurityError < StandardError; end
|
10
|
-
|
11
|
-
module Middleware
|
12
|
-
# Validation middleware that provides input sanitization and security checks
|
13
|
-
# for command execution contexts.
|
14
|
-
#
|
15
|
-
# This middleware validates and sanitizes:
|
16
|
-
# - Command strings for shell injection attacks
|
17
|
-
# - File paths for directory traversal
|
18
|
-
# - URLs for malicious schemes
|
19
|
-
# - Environment variables for sensitive data
|
20
|
-
# - Arguments for dangerous patterns
|
21
|
-
#
|
22
|
-
# @example Basic usage
|
23
|
-
# validator = Commands::Middleware::Validator.new
|
24
|
-
# context = Commands::Context.new(command: 'ls -la')
|
25
|
-
# validated_context = validator.call(context)
|
26
|
-
#
|
27
|
-
# @example With custom validation rules
|
28
|
-
# validator = Commands::Middleware::Validator.new(
|
29
|
-
# allow_shell_operators: false,
|
30
|
-
# blocked_commands: %w[rm sudo],
|
31
|
-
# max_command_length: 500
|
32
|
-
# )
|
33
|
-
class Validator
|
34
|
-
# Default blocked commands that pose security risks
|
35
|
-
DEFAULT_BLOCKED_COMMANDS = %w[
|
36
|
-
rm rmdir
|
37
|
-
sudo su
|
38
|
-
chmod chown
|
39
|
-
wget curl
|
40
|
-
eval exec
|
41
|
-
python ruby node
|
42
|
-
ssh scp rsync
|
43
|
-
dd fdisk mount umount
|
44
|
-
iptables ufw
|
45
|
-
systemctl service
|
46
|
-
crontab
|
47
|
-
passwd
|
48
|
-
].freeze
|
49
|
-
|
50
|
-
# Dangerous shell operators and patterns
|
51
|
-
DANGEROUS_PATTERNS = [
|
52
|
-
/[;&|`$(){}]/, # Shell operators and command substitution
|
53
|
-
%r{\.\./}, # Directory traversal
|
54
|
-
%r{/etc/|/proc/|/sys/}, # Sensitive system directories
|
55
|
-
/\$\{[^}]*\}/, # Variable expansion
|
56
|
-
%r{~[^/\s]*}, # User home directory expansion
|
57
|
-
%r{\bfile://|ftp://|https?://[^\s]*\.(sh|py|rb|js|exe|bat)\b}i, # Executable URLs
|
58
|
-
].freeze
|
59
|
-
|
60
|
-
# Maximum allowed lengths for various inputs
|
61
|
-
DEFAULT_LIMITS = {
|
62
|
-
command_length: 1000,
|
63
|
-
argument_length: 500,
|
64
|
-
path_length: 1000,
|
65
|
-
env_var_value_length: 2000,
|
66
|
-
}.freeze
|
67
|
-
|
68
|
-
attr_reader :options
|
69
|
-
|
70
|
-
# Initialize the validator with security options
|
71
|
-
#
|
72
|
-
# @param options [Hash] Configuration options
|
73
|
-
# @option options [Boolean] :allow_shell_operators (false) Allow shell operators like ;, |, &
|
74
|
-
# @option options [Array<String>] :blocked_commands Commands to block
|
75
|
-
# @option options [Hash] :limits Length limits for various inputs
|
76
|
-
# @option options [Boolean] :strict_mode (true) Enable strict validation
|
77
|
-
# @option options [Array<String>] :allowed_schemes URL schemes to allow
|
78
|
-
def initialize(options = {})
|
79
|
-
@options = {
|
80
|
-
allow_shell_operators: false,
|
81
|
-
blocked_commands: DEFAULT_BLOCKED_COMMANDS,
|
82
|
-
limits: DEFAULT_LIMITS,
|
83
|
-
strict_mode: true,
|
84
|
-
allowed_schemes: %w[http https file],
|
85
|
-
allow_environment_access: false,
|
86
|
-
}.merge(options)
|
87
|
-
end
|
88
|
-
|
89
|
-
# Validate and sanitize the command context
|
90
|
-
#
|
91
|
-
# @param context [Commands::Context] The command execution context
|
92
|
-
# @return [Commands::Context] Validated context
|
93
|
-
# @raise [Commands::SecurityError] If validation fails
|
94
|
-
def call(context)
|
95
|
-
validate_command!(context.command) if context.command
|
96
|
-
validate_arguments!(context.arguments) if context.arguments
|
97
|
-
validate_environment!(context.environment) if context.environment
|
98
|
-
validate_working_directory!(context.working_directory) if context.working_directory
|
99
|
-
|
100
|
-
context
|
101
|
-
rescue StandardError => e
|
102
|
-
raise Commands::SecurityError, "Validation failed: #{e.message}"
|
103
|
-
end
|
104
|
-
|
105
|
-
private
|
106
|
-
|
107
|
-
# Validate the main command string
|
108
|
-
def validate_command!(command)
|
109
|
-
raise Commands::SecurityError, "Empty command" if command.nil? || command.strip.empty?
|
110
|
-
|
111
|
-
if command.length > limits[:command_length]
|
112
|
-
raise Commands::SecurityError,
|
113
|
-
"Command too long (#{command.length} > #{limits[:command_length]})"
|
114
|
-
end
|
115
|
-
|
116
|
-
# Check for blocked commands
|
117
|
-
command_parts = Shellwords.split(command)
|
118
|
-
base_command = File.basename(command_parts.first || "")
|
119
|
-
|
120
|
-
raise Commands::SecurityError, "Blocked command: #{base_command}" if blocked_commands.include?(base_command)
|
121
|
-
|
122
|
-
# Check for dangerous patterns
|
123
|
-
unless options[:allow_shell_operators]
|
124
|
-
DANGEROUS_PATTERNS.each do |pattern|
|
125
|
-
if command.match?(pattern)
|
126
|
-
raise Commands::SecurityError, "Dangerous pattern detected in command: #{pattern.inspect}"
|
127
|
-
end
|
128
|
-
end
|
129
|
-
end
|
130
|
-
|
131
|
-
# Validate shell escaping
|
132
|
-
begin
|
133
|
-
Shellwords.split(command)
|
134
|
-
rescue ArgumentError => e
|
135
|
-
raise Commands::SecurityError, "Invalid shell command syntax: #{e.message}"
|
136
|
-
end
|
137
|
-
end
|
138
|
-
|
139
|
-
# Validate command arguments
|
140
|
-
def validate_arguments!(arguments)
|
141
|
-
return unless arguments
|
142
|
-
|
143
|
-
arguments.each_with_index do |arg, index|
|
144
|
-
next unless arg
|
145
|
-
|
146
|
-
if arg.length > limits[:argument_length]
|
147
|
-
raise Commands::SecurityError, "Argument #{index} too long (#{arg.length} > #{limits[:argument_length]})"
|
148
|
-
end
|
149
|
-
|
150
|
-
# Check for dangerous patterns in arguments
|
151
|
-
if options[:strict_mode]
|
152
|
-
DANGEROUS_PATTERNS.each do |pattern|
|
153
|
-
if arg.match?(pattern)
|
154
|
-
raise Commands::SecurityError, "Dangerous pattern in argument #{index}: #{pattern.inspect}"
|
155
|
-
end
|
156
|
-
end
|
157
|
-
end
|
158
|
-
|
159
|
-
# Validate file paths in arguments
|
160
|
-
validate_path!(arg) if looks_like_path?(arg)
|
161
|
-
|
162
|
-
# Validate URLs in arguments
|
163
|
-
validate_url!(arg) if looks_like_url?(arg)
|
164
|
-
end
|
165
|
-
end
|
166
|
-
|
167
|
-
# Validate environment variables
|
168
|
-
def validate_environment!(environment)
|
169
|
-
return unless environment
|
170
|
-
|
171
|
-
environment.each do |key, value|
|
172
|
-
next unless value
|
173
|
-
|
174
|
-
if value.to_s.length > limits[:env_var_value_length]
|
175
|
-
raise Commands::SecurityError, "Environment variable #{key} value too long"
|
176
|
-
end
|
177
|
-
|
178
|
-
# Check for sensitive environment variables
|
179
|
-
if sensitive_env_var?(key) && !options[:allow_environment_access]
|
180
|
-
raise Commands::SecurityError, "Access to sensitive environment variable #{key} not allowed"
|
181
|
-
end
|
182
|
-
|
183
|
-
# Validate environment variable values
|
184
|
-
if options[:strict_mode] && contains_dangerous_content?(value.to_s)
|
185
|
-
raise Commands::SecurityError, "Dangerous content in environment variable #{key}"
|
186
|
-
end
|
187
|
-
end
|
188
|
-
end
|
189
|
-
|
190
|
-
# Validate working directory path
|
191
|
-
def validate_working_directory!(path)
|
192
|
-
validate_path!(path, "working directory")
|
193
|
-
end
|
194
|
-
|
195
|
-
# Validate file system paths
|
196
|
-
def validate_path!(path, context = "path")
|
197
|
-
return if path.nil? || path.empty?
|
198
|
-
|
199
|
-
if path.length > limits[:path_length]
|
200
|
-
raise Commands::SecurityError, "#{context.capitalize} too long (#{path.length} > #{limits[:path_length]})"
|
201
|
-
end
|
202
|
-
|
203
|
-
# Check for directory traversal
|
204
|
-
normalized_path = File.expand_path(path)
|
205
|
-
if path.include?("..") && options[:strict_mode]
|
206
|
-
raise Commands::SecurityError, "Directory traversal detected in #{context}: #{path}"
|
207
|
-
end
|
208
|
-
|
209
|
-
# Check for access to sensitive directories
|
210
|
-
sensitive_dirs = %w[/etc /proc /sys /dev /boot /root]
|
211
|
-
return unless sensitive_dirs.any? { |dir| normalized_path.start_with?(dir) } && options[:strict_mode]
|
212
|
-
|
213
|
-
raise Commands::SecurityError, "Access to sensitive directory in #{context}: #{path}"
|
214
|
-
end
|
215
|
-
|
216
|
-
# Validate URLs
|
217
|
-
def validate_url!(url)
|
218
|
-
uri = URI.parse(url)
|
219
|
-
|
220
|
-
unless options[:allowed_schemes].include?(uri.scheme)
|
221
|
-
raise Commands::SecurityError, "Disallowed URL scheme: #{uri.scheme}"
|
222
|
-
end
|
223
|
-
|
224
|
-
# Check for suspicious hosts
|
225
|
-
if uri.host&.match?(/localhost|127\.0\.0\.1|0\.0\.0\.0/)
|
226
|
-
raise Commands::SecurityError, "Local network access not allowed: #{uri.host}"
|
227
|
-
end
|
228
|
-
rescue URI::InvalidURIError => e
|
229
|
-
raise Commands::SecurityError, "Invalid URL format: #{e.message}"
|
230
|
-
end
|
231
|
-
|
232
|
-
# Check if string looks like a file path
|
233
|
-
def looks_like_path?(str)
|
234
|
-
str.start_with?("/") || str.start_with?("./") || str.start_with?("../") || str.include?(File::SEPARATOR)
|
235
|
-
end
|
236
|
-
|
237
|
-
# Check if string looks like a URL
|
238
|
-
def looks_like_url?(str)
|
239
|
-
str.match?(/\A[a-z][a-z0-9+.-]*:/i)
|
240
|
-
end
|
241
|
-
|
242
|
-
# Check if environment variable is sensitive
|
243
|
-
def sensitive_env_var?(key)
|
244
|
-
sensitive_patterns = %w[
|
245
|
-
PASSWORD SECRET TOKEN KEY API PRIVATE
|
246
|
-
AWS GCP AZURE GITHUB GITLAB
|
247
|
-
DATABASE_URL DB_PASSWORD
|
248
|
-
RAILS_MASTER_KEY
|
249
|
-
]
|
250
|
-
|
251
|
-
sensitive_patterns.any? { |pattern| key.to_s.upcase.include?(pattern) }
|
252
|
-
end
|
253
|
-
|
254
|
-
# Check if content contains dangerous patterns
|
255
|
-
def contains_dangerous_content?(content)
|
256
|
-
DANGEROUS_PATTERNS.any? { |pattern| content.match?(pattern) }
|
257
|
-
end
|
258
|
-
|
259
|
-
def limits
|
260
|
-
options[:limits]
|
261
|
-
end
|
262
|
-
|
263
|
-
def blocked_commands
|
264
|
-
options[:blocked_commands]
|
265
|
-
end
|
266
|
-
end
|
267
|
-
end
|
268
|
-
end
|
269
|
-
end
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "shellwords"
|
4
|
+
require "uri"
|
5
|
+
|
6
|
+
module Makit
|
7
|
+
module Commands
|
8
|
+
# Custom exception for security-related command validation failures
|
9
|
+
class SecurityError < StandardError; end
|
10
|
+
|
11
|
+
module Middleware
|
12
|
+
# Validation middleware that provides input sanitization and security checks
|
13
|
+
# for command execution contexts.
|
14
|
+
#
|
15
|
+
# This middleware validates and sanitizes:
|
16
|
+
# - Command strings for shell injection attacks
|
17
|
+
# - File paths for directory traversal
|
18
|
+
# - URLs for malicious schemes
|
19
|
+
# - Environment variables for sensitive data
|
20
|
+
# - Arguments for dangerous patterns
|
21
|
+
#
|
22
|
+
# @example Basic usage
|
23
|
+
# validator = Commands::Middleware::Validator.new
|
24
|
+
# context = Commands::Context.new(command: 'ls -la')
|
25
|
+
# validated_context = validator.call(context)
|
26
|
+
#
|
27
|
+
# @example With custom validation rules
|
28
|
+
# validator = Commands::Middleware::Validator.new(
|
29
|
+
# allow_shell_operators: false,
|
30
|
+
# blocked_commands: %w[rm sudo],
|
31
|
+
# max_command_length: 500
|
32
|
+
# )
|
33
|
+
class Validator
|
34
|
+
# Default blocked commands that pose security risks
|
35
|
+
DEFAULT_BLOCKED_COMMANDS = %w[
|
36
|
+
rm rmdir
|
37
|
+
sudo su
|
38
|
+
chmod chown
|
39
|
+
wget curl
|
40
|
+
eval exec
|
41
|
+
python ruby node
|
42
|
+
ssh scp rsync
|
43
|
+
dd fdisk mount umount
|
44
|
+
iptables ufw
|
45
|
+
systemctl service
|
46
|
+
crontab
|
47
|
+
passwd
|
48
|
+
].freeze
|
49
|
+
|
50
|
+
# Dangerous shell operators and patterns
|
51
|
+
DANGEROUS_PATTERNS = [
|
52
|
+
/[;&|`$(){}]/, # Shell operators and command substitution
|
53
|
+
%r{\.\./}, # Directory traversal
|
54
|
+
%r{/etc/|/proc/|/sys/}, # Sensitive system directories
|
55
|
+
/\$\{[^}]*\}/, # Variable expansion
|
56
|
+
%r{~[^/\s]*}, # User home directory expansion
|
57
|
+
%r{\bfile://|ftp://|https?://[^\s]*\.(sh|py|rb|js|exe|bat)\b}i, # Executable URLs
|
58
|
+
].freeze
|
59
|
+
|
60
|
+
# Maximum allowed lengths for various inputs
|
61
|
+
DEFAULT_LIMITS = {
|
62
|
+
command_length: 1000,
|
63
|
+
argument_length: 500,
|
64
|
+
path_length: 1000,
|
65
|
+
env_var_value_length: 2000,
|
66
|
+
}.freeze
|
67
|
+
|
68
|
+
attr_reader :options
|
69
|
+
|
70
|
+
# Initialize the validator with security options
|
71
|
+
#
|
72
|
+
# @param options [Hash] Configuration options
|
73
|
+
# @option options [Boolean] :allow_shell_operators (false) Allow shell operators like ;, |, &
|
74
|
+
# @option options [Array<String>] :blocked_commands Commands to block
|
75
|
+
# @option options [Hash] :limits Length limits for various inputs
|
76
|
+
# @option options [Boolean] :strict_mode (true) Enable strict validation
|
77
|
+
# @option options [Array<String>] :allowed_schemes URL schemes to allow
|
78
|
+
def initialize(options = {})
|
79
|
+
@options = {
|
80
|
+
allow_shell_operators: false,
|
81
|
+
blocked_commands: DEFAULT_BLOCKED_COMMANDS,
|
82
|
+
limits: DEFAULT_LIMITS,
|
83
|
+
strict_mode: true,
|
84
|
+
allowed_schemes: %w[http https file],
|
85
|
+
allow_environment_access: false,
|
86
|
+
}.merge(options)
|
87
|
+
end
|
88
|
+
|
89
|
+
# Validate and sanitize the command context
|
90
|
+
#
|
91
|
+
# @param context [Commands::Context] The command execution context
|
92
|
+
# @return [Commands::Context] Validated context
|
93
|
+
# @raise [Commands::SecurityError] If validation fails
|
94
|
+
def call(context)
|
95
|
+
validate_command!(context.command) if context.command
|
96
|
+
validate_arguments!(context.arguments) if context.arguments
|
97
|
+
validate_environment!(context.environment) if context.environment
|
98
|
+
validate_working_directory!(context.working_directory) if context.working_directory
|
99
|
+
|
100
|
+
context
|
101
|
+
rescue StandardError => e
|
102
|
+
raise Commands::SecurityError, "Validation failed: #{e.message}"
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
# Validate the main command string
|
108
|
+
def validate_command!(command)
|
109
|
+
raise Commands::SecurityError, "Empty command" if command.nil? || command.strip.empty?
|
110
|
+
|
111
|
+
if command.length > limits[:command_length]
|
112
|
+
raise Commands::SecurityError,
|
113
|
+
"Command too long (#{command.length} > #{limits[:command_length]})"
|
114
|
+
end
|
115
|
+
|
116
|
+
# Check for blocked commands
|
117
|
+
command_parts = Shellwords.split(command)
|
118
|
+
base_command = File.basename(command_parts.first || "")
|
119
|
+
|
120
|
+
raise Commands::SecurityError, "Blocked command: #{base_command}" if blocked_commands.include?(base_command)
|
121
|
+
|
122
|
+
# Check for dangerous patterns
|
123
|
+
unless options[:allow_shell_operators]
|
124
|
+
DANGEROUS_PATTERNS.each do |pattern|
|
125
|
+
if command.match?(pattern)
|
126
|
+
raise Commands::SecurityError, "Dangerous pattern detected in command: #{pattern.inspect}"
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# Validate shell escaping
|
132
|
+
begin
|
133
|
+
Shellwords.split(command)
|
134
|
+
rescue ArgumentError => e
|
135
|
+
raise Commands::SecurityError, "Invalid shell command syntax: #{e.message}"
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# Validate command arguments
|
140
|
+
def validate_arguments!(arguments)
|
141
|
+
return unless arguments
|
142
|
+
|
143
|
+
arguments.each_with_index do |arg, index|
|
144
|
+
next unless arg
|
145
|
+
|
146
|
+
if arg.length > limits[:argument_length]
|
147
|
+
raise Commands::SecurityError, "Argument #{index} too long (#{arg.length} > #{limits[:argument_length]})"
|
148
|
+
end
|
149
|
+
|
150
|
+
# Check for dangerous patterns in arguments
|
151
|
+
if options[:strict_mode]
|
152
|
+
DANGEROUS_PATTERNS.each do |pattern|
|
153
|
+
if arg.match?(pattern)
|
154
|
+
raise Commands::SecurityError, "Dangerous pattern in argument #{index}: #{pattern.inspect}"
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# Validate file paths in arguments
|
160
|
+
validate_path!(arg) if looks_like_path?(arg)
|
161
|
+
|
162
|
+
# Validate URLs in arguments
|
163
|
+
validate_url!(arg) if looks_like_url?(arg)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
# Validate environment variables
|
168
|
+
def validate_environment!(environment)
|
169
|
+
return unless environment
|
170
|
+
|
171
|
+
environment.each do |key, value|
|
172
|
+
next unless value
|
173
|
+
|
174
|
+
if value.to_s.length > limits[:env_var_value_length]
|
175
|
+
raise Commands::SecurityError, "Environment variable #{key} value too long"
|
176
|
+
end
|
177
|
+
|
178
|
+
# Check for sensitive environment variables
|
179
|
+
if sensitive_env_var?(key) && !options[:allow_environment_access]
|
180
|
+
raise Commands::SecurityError, "Access to sensitive environment variable #{key} not allowed"
|
181
|
+
end
|
182
|
+
|
183
|
+
# Validate environment variable values
|
184
|
+
if options[:strict_mode] && contains_dangerous_content?(value.to_s)
|
185
|
+
raise Commands::SecurityError, "Dangerous content in environment variable #{key}"
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
# Validate working directory path
|
191
|
+
def validate_working_directory!(path)
|
192
|
+
validate_path!(path, "working directory")
|
193
|
+
end
|
194
|
+
|
195
|
+
# Validate file system paths
|
196
|
+
def validate_path!(path, context = "path")
|
197
|
+
return if path.nil? || path.empty?
|
198
|
+
|
199
|
+
if path.length > limits[:path_length]
|
200
|
+
raise Commands::SecurityError, "#{context.capitalize} too long (#{path.length} > #{limits[:path_length]})"
|
201
|
+
end
|
202
|
+
|
203
|
+
# Check for directory traversal
|
204
|
+
normalized_path = File.expand_path(path)
|
205
|
+
if path.include?("..") && options[:strict_mode]
|
206
|
+
raise Commands::SecurityError, "Directory traversal detected in #{context}: #{path}"
|
207
|
+
end
|
208
|
+
|
209
|
+
# Check for access to sensitive directories
|
210
|
+
sensitive_dirs = %w[/etc /proc /sys /dev /boot /root]
|
211
|
+
return unless sensitive_dirs.any? { |dir| normalized_path.start_with?(dir) } && options[:strict_mode]
|
212
|
+
|
213
|
+
raise Commands::SecurityError, "Access to sensitive directory in #{context}: #{path}"
|
214
|
+
end
|
215
|
+
|
216
|
+
# Validate URLs
|
217
|
+
def validate_url!(url)
|
218
|
+
uri = URI.parse(url)
|
219
|
+
|
220
|
+
unless options[:allowed_schemes].include?(uri.scheme)
|
221
|
+
raise Commands::SecurityError, "Disallowed URL scheme: #{uri.scheme}"
|
222
|
+
end
|
223
|
+
|
224
|
+
# Check for suspicious hosts
|
225
|
+
if uri.host&.match?(/localhost|127\.0\.0\.1|0\.0\.0\.0/)
|
226
|
+
raise Commands::SecurityError, "Local network access not allowed: #{uri.host}"
|
227
|
+
end
|
228
|
+
rescue URI::InvalidURIError => e
|
229
|
+
raise Commands::SecurityError, "Invalid URL format: #{e.message}"
|
230
|
+
end
|
231
|
+
|
232
|
+
# Check if string looks like a file path
|
233
|
+
def looks_like_path?(str)
|
234
|
+
str.start_with?("/") || str.start_with?("./") || str.start_with?("../") || str.include?(File::SEPARATOR)
|
235
|
+
end
|
236
|
+
|
237
|
+
# Check if string looks like a URL
|
238
|
+
def looks_like_url?(str)
|
239
|
+
str.match?(/\A[a-z][a-z0-9+.-]*:/i)
|
240
|
+
end
|
241
|
+
|
242
|
+
# Check if environment variable is sensitive
|
243
|
+
def sensitive_env_var?(key)
|
244
|
+
sensitive_patterns = %w[
|
245
|
+
PASSWORD SECRET TOKEN KEY API PRIVATE
|
246
|
+
AWS GCP AZURE GITHUB GITLAB
|
247
|
+
DATABASE_URL DB_PASSWORD
|
248
|
+
RAILS_MASTER_KEY
|
249
|
+
]
|
250
|
+
|
251
|
+
sensitive_patterns.any? { |pattern| key.to_s.upcase.include?(pattern) }
|
252
|
+
end
|
253
|
+
|
254
|
+
# Check if content contains dangerous patterns
|
255
|
+
def contains_dangerous_content?(content)
|
256
|
+
DANGEROUS_PATTERNS.any? { |pattern| content.match?(pattern) }
|
257
|
+
end
|
258
|
+
|
259
|
+
def limits
|
260
|
+
options[:limits]
|
261
|
+
end
|
262
|
+
|
263
|
+
def blocked_commands
|
264
|
+
options[:blocked_commands]
|
265
|
+
end
|
266
|
+
end
|
267
|
+
end
|
268
|
+
end
|
269
|
+
end
|